@torrent-tv/proxy 2.80.17 → 2.80.19
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 +16 -0
- package/docs/encode-architecture.md +91 -7
- package/package.json +1 -1
- package/services/encode/EncodePlan.js +22 -12
- package/services/encode/EncodeRun.js +12 -6
- package/services/encode/SegmentStore.js +147 -223
- package/services/encode/run-command.js +16 -1
- package/services/encode/run-costs.js +15 -40
- package/services/hls-session-manager.js +5 -83
- package/services/orchestrators/EncodeOrchestrator.js +63 -36
- package/services/segment-formats/fmp4.js +54 -0
- package/services/segment-formats/mpegts.js +54 -0
- package/test/coverage-follows-the-disk.test.js +5 -1
- package/test/move-cost.test.js +109 -38
- package/test/orchestrator-wired.test.js +5 -1
- package/test/produced-copy-choice.test.js +258 -358
- package/test/segment-serve-wiring.test.js +8 -9
- package/test/segment-store.test.js +238 -216
- package/test/split-a-stretch.test.js +139 -0
- package/services/encode/open-piece.js +0 -135
- package/test/open-piece.test.js +0 -152
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 2.80.19
|
|
2
|
+
|
|
3
|
+
- **Fix**: HALF A SEGMENT WAS SERVED TO THE PLAYER, AND THE SESSION NEVER RECOVERED. A piece was taken as finished when the NEXT number existed. That is sound for one writer walking forward and false the moment two runs share an output — which is what the plan gives an output whenever it places a second encoder, and what a one-segment interval guarantees. Field 2026-09-08: `segment-00057.mp4` was served at 2 268 361 bytes and then at 4 510 940, exactly half; `segment-00055.mp4` at 211 957 and then 2 620 617. The browser appended the half, refused the whole one for the rest of the session and repeated `bufferAppendError` to the end of the log with the picture frozen at 319.66 s.
|
|
4
|
+
**A piece's NAME is the proof now, and there is no second one.** It is written as `making-<from>-00057.mp4` — tagged with the first number of its run's stretch — and takes `segment-00057.mp4` when the encoder says on its own channel that it has closed it, which is one rename inside one directory. The `hls` branch needed nothing added: its muxer already writes through a temporary name of its own. One rule for both branches, and true whether or not this process is alive.
|
|
5
|
+
- **Fix**: The last piece of every run is provable. Under the successor rule nothing followed it, so it never was — which is the resume case that held a segment for 46 s and then answered 404 to a browser that had given up.
|
|
6
|
+
- **Fix**: Clearing up after a dead run is a name match against that run's own tag: no stretch to search, no bytes to judge, and no way to remove a complete piece another run closed. `services/encode/open-piece.js` did all three by guessing, and under the naming rule its guess would have removed a finished segment — the highest served name in a dead run's stretch is a piece that run closed. It is deleted, together with the session manager's own copy of it, which fetched the output's init bytes to judge a file by its contents. 135 lines of service code and 152 of tests for the guessing are gone; the manager is 83 lines shorter and 5 longer.
|
|
7
|
+
- **Fix**: One owner for "this piece is closed". A set of statements was kept beside the disk and told by whoever noticed a piece being produced — the same two-owner fault 2.80.11 removed from the coverage map, one layer down. A rename cannot go out of step with itself.
|
|
8
|
+
- **New**: `encode-plan on <output>` carries every term the decision was made from: `[speed=4.45x firstByte=1.26s kill=0.04s refetch=0.000s/s maxRuns=2 live=1]`. An interval says what was decided; only these say why. A decision of this plan is `delay + (index - at) / rate + madeBetween * refetch` against a deadline, so a recorded decision without the rate can be re-read and not recomputed — which is exactly what happened with the one-piece intervals of 2026-09-08, where the rate was substituted six times from the speeds the session reported elsewhere and none of them gave the answer the plan had given. A zero in the last three is a measurement nobody has taken, not a free operation, and it is printed so that reading it as free is a choice.
|
|
9
|
+
- **Chore**: 965 checks pass, biome clean. NOT yet seen in the field. What the next session must show: `making-*` files present in an output's directory while it is being written and none left after it; no `bufferAppendError` following a segment served twice at different sizes; and the terms above beside every placement.
|
|
10
|
+
|
|
11
|
+
## 2.80.18
|
|
12
|
+
|
|
13
|
+
- **Fix**: The `Infinity` is gone, and with it a double count I had introduced. The measured "time to a first piece" already CONTAINS one piece's encoding, and I was adding another; separated, the two scale differently — a piece costs more when encoders share the machine, a spawn does not. So a fresh encoder owes `spawn overhead + the piece at the rate in force`, a moved one owes the kill and then the same. With nothing measured the overhead is zero and a fresh encoder owes exactly one piece, which is the floor and is DERIVED rather than chosen: a piece cannot appear before it is encoded, and how fast this host encodes is measured before any viewer exists. The `Infinity` was an exception in a model that needs none. Verified by simulation over eighty ticks against the map's real shape — zero moves and zero one-piece intervals at a budget of one encoder and of three.
|
|
14
|
+
- **New**: `encode-plan on <output>: start #58..#481, stop #?..#?` — every action with its INTERVAL, which is what a run is, printed on any pass that does something. Its absence cost three wrong diagnoses of one field session: the line printed the windows, the budget and where the live runs stood, and not the intervals the actions carried.
|
|
15
|
+
- **Chore**: THE FIELD DIAGNOSIS OF 2026-09-08, corrected. What that session did was give every encoder an interval of exactly ONE segment — 63 runs, each spending 1.26 s reaching its first piece, making that one piece, reaching the end of its interval and exiting, twelve of them normally and thirteen killed. The reasons printed beside them read as moves back and forth, so I diagnosed an oscillating placement three times over and shipped 2.80.15 through 2.80.17 against it. Those releases stand on their own — the objective now follows the map's own rank order, the arrival arithmetic no longer charges a run for a piece it is already making, and the split point is derived — but none of them touches the caravan. Which line assigns an interval of one segment is not yet named: on a clean model it does not happen, three candidate explanations were checked and all three fell, and the line above is what will name it on the next session.
|
|
16
|
+
|
|
1
17
|
## 2.80.17
|
|
2
18
|
|
|
3
19
|
- **Fix**: Where a second encoder joins a stretch is DERIVED, and whether it is worth having is asked at all. It was the midpoint of the widest unmade run, unconditionally. Both encoders close their halves at a time that rises with the split point for one and falls for the other, so the stretch is closed soonest where they cross: `x* = (from + to + 1)/2 + (d - w) * r / 2` — the midpoint, shifted forward by half the difference between what a fresh encoder owes and what the one already there owes, in pieces. Halving is the special case where both are fresh and owe the same.
|
|
@@ -242,12 +242,28 @@ piece costs — because only there is it known what the body IS, and at the poin
|
|
|
242
242
|
of pricing only how many there are. So there is no kind, no tag and no case
|
|
243
243
|
analysis.
|
|
244
244
|
|
|
245
|
-
**
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
**The measured start is separated from the piece it contains**, because the two
|
|
246
|
+
scale differently: a piece costs more when encoders share the machine, a spawn
|
|
247
|
+
does not.
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
spawn overhead = measured first output - what one piece costs alone
|
|
251
|
+
a fresh encoder = spawn overhead + the piece at the rate in force
|
|
252
|
+
a moved one = the kill, and then the same
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
With nothing measured the overhead is zero and a fresh encoder owes exactly one
|
|
256
|
+
piece. **That is the floor, and it is derived rather than chosen:** a piece
|
|
257
|
+
cannot appear before it is encoded, and how fast this host encodes is measured
|
|
258
|
+
before any viewer exists. There was an `Infinity` here for the cost of a move,
|
|
259
|
+
on the reasoning that an unmeasured price must not license an irreversible act —
|
|
260
|
+
an exception in a model that needs none, and this is the same statement made by
|
|
261
|
+
arithmetic.
|
|
262
|
+
|
|
263
|
+
And a run killed before producing anything is a measurement too — a lower bound
|
|
264
|
+
on the first output, and the only reading a thrash can supply, since every run in
|
|
265
|
+
one is killed before it finishes anything. So a thrash makes its own moves
|
|
266
|
+
progressively dearer until it stops.
|
|
251
267
|
|
|
252
268
|
### What that was for
|
|
253
269
|
|
|
@@ -262,7 +278,7 @@ IMMEDIATELY and was the most urgent material in the file: it bought encoders, an
|
|
|
262
278
|
it took the run standing in front of the viewer because that run was the nearest
|
|
263
279
|
body to it.
|
|
264
280
|
|
|
265
|
-
Checked by simulation over
|
|
281
|
+
Checked by simulation over eighty ticks against the map's real shape — ten zones
|
|
266
282
|
doubling ahead of the viewer, one behind — at both one and three runs: the
|
|
267
283
|
encoder is placed once, left alone, and moved exactly once, at the viewer's own
|
|
268
284
|
seek.
|
|
@@ -301,6 +317,69 @@ penalty for a second encoder on the addon host is 1.98 — it takes very nearly
|
|
|
301
317
|
all of the first's speed — and the objective keeps one; where a second is free it
|
|
302
318
|
places three.
|
|
303
319
|
|
|
320
|
+
## What the log says when anything is placed or taken away
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
encode-plan on <output>: start #58..#481, stop #?..#?
|
|
324
|
+
[speed=4.45x firstByte=1.26s kill=0.04s refetch=0.000s/s maxRuns=2 live=1]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Every action with its INTERVAL, which is what a run is, and then every term the
|
|
328
|
+
decision was made from. Printed on any pass that does something — a session
|
|
329
|
+
where nothing changes says nothing.
|
|
330
|
+
|
|
331
|
+
The terms are there because an interval says WHAT was decided and only these say
|
|
332
|
+
WHY. A decision of this plan is
|
|
333
|
+
|
|
334
|
+
delay + (index - at) / rate + madeBetween * refetch against a deadline
|
|
335
|
+
|
|
336
|
+
so a recorded decision without the rate can be re-read and not recomputed. That
|
|
337
|
+
is not hypothetical: the one-piece intervals of 2026-09-08 were diagnosed by
|
|
338
|
+
substituting the rate from the speeds the session reported elsewhere — six
|
|
339
|
+
different figures, none of which reproduced the answer the plan had given.
|
|
340
|
+
|
|
341
|
+
A zero in `firstByte`, `kill` or `refetch` is a measurement nobody has taken, not
|
|
342
|
+
a free operation. It is printed so that reading it as free is a choice.
|
|
343
|
+
|
|
344
|
+
It was missing, and its absence cost three wrong diagnoses of one field session.
|
|
345
|
+
The line printed the windows, the budget and where the live runs stood; the
|
|
346
|
+
intervals the actions carried were the one thing it did not print. What that
|
|
347
|
+
session actually did was give every encoder an interval of exactly ONE segment —
|
|
348
|
+
63 runs, each spending 1.26 s reaching its first piece, making that one piece,
|
|
349
|
+
reaching the end of its interval and exiting, twelve of them normally. The
|
|
350
|
+
reasons printed beside them read as moves back and forth, so the fault was read
|
|
351
|
+
as an oscillating placement three times over. An interval of one segment turns
|
|
352
|
+
the protection against two encoders writing one name into a mill for processes.
|
|
353
|
+
|
|
354
|
+
## What proves a segment is finished
|
|
355
|
+
|
|
356
|
+
Its NAME, and there is nothing else. A piece being written is called
|
|
357
|
+
`making-<from>-00042.mp4` — the tag is the first number of the stretch its run
|
|
358
|
+
was given — and it takes `segment-00042.mp4` when the encoder says it has closed
|
|
359
|
+
it, which it does on a channel of its own (`-segment_list pipe:3`). Making it
|
|
360
|
+
servable is therefore one rename inside one directory, performed by the store
|
|
361
|
+
because the store owns the disk. The `hls` branch needs nothing extra: its muxer
|
|
362
|
+
writes through a temporary name of its own, so its files appear under their final
|
|
363
|
+
name whole.
|
|
364
|
+
|
|
365
|
+
Three things follow, and each replaced a guess:
|
|
366
|
+
|
|
367
|
+
1. **a request can never reach a half-written piece.** Closure used to be
|
|
368
|
+
inferred from the NEXT number existing — sound for one writer walking forward,
|
|
369
|
+
false the moment two runs share an output, which is what the plan gives an
|
|
370
|
+
output whenever it places a second encoder. Field 2026-09-08:
|
|
371
|
+
`segment-00057.mp4` served at 2 268 361 bytes and then at 4 510 940, exactly
|
|
372
|
+
half; the browser appended the half and refused the whole for the rest of the
|
|
373
|
+
session, with the picture frozen at 319.66 s;
|
|
374
|
+
2. **the last piece of a run is provable.** Under the successor rule nothing
|
|
375
|
+
followed it, so it never was — the resume case that held one segment for 46 s
|
|
376
|
+
and then answered 404;
|
|
377
|
+
3. **clearing up after a dead run is a name match.** Its unfinished pieces are
|
|
378
|
+
the ones carrying its own tag: no stretch to search, no bytes to judge, and no
|
|
379
|
+
way to remove a complete piece somebody else closed. `services/encode/
|
|
380
|
+
open-piece.js` did all three of those by guessing and is gone, along with the
|
|
381
|
+
session manager's copy of it.
|
|
382
|
+
|
|
304
383
|
## What is checked
|
|
305
384
|
|
|
306
385
|
`test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
|
|
@@ -313,3 +392,8 @@ viewer registry, the real `LiveOutputs` and the real `PriorityOrchestrator`.
|
|
|
313
392
|
|
|
314
393
|
`test/encode-plan.test.js` holds the arithmetic, including that every encoder
|
|
315
394
|
stops when nobody is watching the output.
|
|
395
|
+
|
|
396
|
+
`test/segment-store.test.js` holds the naming rule: a piece under its served
|
|
397
|
+
name is finished — the last one of a run included — one under a working name is
|
|
398
|
+
not and cannot be reached, closing it is one rename, and clearing up after one
|
|
399
|
+
run leaves every other run's work alone.
|
package/package.json
CHANGED
|
@@ -102,9 +102,6 @@
|
|
|
102
102
|
* measured on this host from its own runs. Zero until something has measured
|
|
103
103
|
* it, which makes moving one look cheaper than it is and is said here so the
|
|
104
104
|
* bias is known.
|
|
105
|
-
* @param {number} [params.moveCostSec] - What moving a running encoder costs on
|
|
106
|
-
* this host, measured. `Infinity` until something has been measured, because a
|
|
107
|
-
* move is irreversible and leaving the encoder alone is always available.
|
|
108
105
|
* @param {number} [params.now] - The clock, injected. This layer is arithmetic
|
|
109
106
|
* and reads no clock of its own; how old a run is is one of its inputs.
|
|
110
107
|
* @param {number} [params.firstByteWaitSec] - How long a fresh encoder takes to
|
|
@@ -128,7 +125,6 @@ export function planEncoders({
|
|
|
128
125
|
segmentSeconds,
|
|
129
126
|
killCostSec = 0,
|
|
130
127
|
firstByteWaitSec = 0,
|
|
131
|
-
moveCostSec = Number.POSITIVE_INFINITY,
|
|
132
128
|
now = Date.now(),
|
|
133
129
|
refetchSecPerFilmSecond = 0,
|
|
134
130
|
contentionPenaltyFor = () => 1,
|
|
@@ -185,12 +181,26 @@ export function planEncoders({
|
|
|
185
181
|
// its death, the start of another, and the wait for the first bytes there.
|
|
186
182
|
// Taking an encoder somewhere else is stopping this one and waiting for the
|
|
187
183
|
// next to produce. Both halves are measured on this host.
|
|
188
|
-
// WHAT A
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
184
|
+
// WHAT A START AND A MOVE OWE BEFORE THE PIECE THEY STAND ON EXISTS, and
|
|
185
|
+
// neither is infinite.
|
|
186
|
+
//
|
|
187
|
+
// The measured `firstByteWaitSec` is spawn to first piece, so it already
|
|
188
|
+
// contains one piece's encoding. Separated, because the two scale differently:
|
|
189
|
+
// the piece costs more when encoders share the machine, the spawn does not.
|
|
190
|
+
//
|
|
191
|
+
// spawn overhead = measured first output - what one piece costs alone
|
|
192
|
+
// a fresh encoder owes spawn overhead + the piece at the rate in force
|
|
193
|
+
// a moved one owes the kill, and then the same
|
|
194
|
+
//
|
|
195
|
+
// With nothing measured the overhead is zero and a fresh encoder owes exactly
|
|
196
|
+
// one piece — which is the honest floor rather than a guess: a piece cannot
|
|
197
|
+
// appear before it is encoded, and how fast this host encodes is measured
|
|
198
|
+
// before any viewer exists. There was an `Infinity` here for the cost of a
|
|
199
|
+
// move, on the reasoning that an unmeasured price must not license an
|
|
200
|
+
// irreversible act; it was an exception in a model that needs none, and this
|
|
201
|
+
// is the same statement made by arithmetic.
|
|
202
|
+
const spawnOverheadSec = Math.max(0, firstByteWaitSec - (rate > 0 ? 1 / rate : 0));
|
|
203
|
+
const moveSec = killCostSec + spawnOverheadSec;
|
|
194
204
|
|
|
195
205
|
// WHAT A RUN STILL HAS TO GO BEFORE IT PRODUCES ANYTHING — the measured time
|
|
196
206
|
// to a first piece, less the time it has already been alive.
|
|
@@ -393,7 +403,7 @@ export function planEncoders({
|
|
|
393
403
|
}
|
|
394
404
|
if (filler === "new") {
|
|
395
405
|
// A encoder that does not exist yet owes its own start and then the piece.
|
|
396
|
-
encoders.push({ at: positions[index], owes: (piece) =>
|
|
406
|
+
encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
|
|
397
407
|
continue;
|
|
398
408
|
}
|
|
399
409
|
const head = Number(filler.head);
|
|
@@ -472,7 +482,7 @@ export function planEncoders({
|
|
|
472
482
|
}
|
|
473
483
|
for (let index = 0; index < positions.length; index += 1) {
|
|
474
484
|
if ((best ? best.fill[index] : null) === "new") {
|
|
475
|
-
encoders.push({ at: positions[index], owes: (piece) =>
|
|
485
|
+
encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
|
|
476
486
|
}
|
|
477
487
|
}
|
|
478
488
|
return priced(encoders);
|
|
@@ -181,9 +181,11 @@ export class EncodeRun {
|
|
|
181
181
|
* @param {{ info: (line: string) => void, warn: (line: string) => void, error?: (line: string) => void }} params.logger
|
|
182
182
|
* @param {() => number} [params.now]
|
|
183
183
|
* @param {(ended: RunEnded) => void} [params.onEnded]
|
|
184
|
-
* @param {(name: string) =>
|
|
185
|
-
* of every piece the encoder has
|
|
186
|
-
* names it on its own channel
|
|
184
|
+
* @param {(name: string) => string | null} [params.onClosed] - Called with the
|
|
185
|
+
* WORKING name of every piece the encoder has finished writing, as the
|
|
186
|
+
* encoder itself names it on its own channel, and answers with the name that
|
|
187
|
+
* piece is served under — because making it servable is a rename, and only
|
|
188
|
+
* whoever owns the disk can perform one.
|
|
187
189
|
* @param {(progress: { processedSeconds: number | null, speed: string | null }) => void} [params.onProgress]
|
|
188
190
|
* Called for every `-progress` report. Seconds count from the START OF THIS
|
|
189
191
|
* RUN on both branches — neither `-output_ts_offset` nor `-copyts` changes
|
|
@@ -473,8 +475,13 @@ export class EncodeRun {
|
|
|
473
475
|
if (name.length === 0) {
|
|
474
476
|
continue;
|
|
475
477
|
}
|
|
478
|
+
// ITS SERVED NAME, which is what whoever owns the disk gives it in answer.
|
|
479
|
+
// ffmpeg writes a piece under a working name and reports that; the piece
|
|
480
|
+
// becomes servable by being renamed, and everything below works in the
|
|
481
|
+
// name a request can actually ask for.
|
|
482
|
+
const served = this.onClosed(name) ?? name;
|
|
476
483
|
if (!this.#stopping) {
|
|
477
|
-
this.#provenName =
|
|
484
|
+
this.#provenName = served;
|
|
478
485
|
}
|
|
479
486
|
// WHAT THIS RUN HAS MADE IS THIS RUN'S OWN FACT, and this channel is where
|
|
480
487
|
// it learns it. It used to be told from outside, by whoever listed the
|
|
@@ -484,11 +491,10 @@ export class EncodeRun {
|
|
|
484
491
|
// (483 segment(s))", having produced none of them, and its head therefore
|
|
485
492
|
// described somebody else's work. Both the claim it holds and the cleanup
|
|
486
493
|
// after it read that head.
|
|
487
|
-
const index = this.indexOfName(
|
|
494
|
+
const index = this.indexOfName(served);
|
|
488
495
|
if (Number.isInteger(index)) {
|
|
489
496
|
this.noteProduced(index);
|
|
490
497
|
}
|
|
491
|
-
this.onClosed(name);
|
|
492
498
|
}
|
|
493
499
|
}
|
|
494
500
|
|