@torrent-tv/proxy 2.30.2 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.31.0
2
+
3
+ - **Chore**: The encoder run's two status strings are gone; both are now outputs of the state table shipped in 2.23.0. `session.progress.state` was maintained by hand at seven sites and `session.state` at nine, and neither could answer on its own — the warm-up test had to read both under an `||`, because one said "starting" from the first spawn until something overwrote it while the other said it again on its own schedule. What the browser is told is computed where it is sent (`wireState(runState)`), and `session.state` is reduced to the session's own lifetime: it exists, or it has been disposed. That deletes the line in the spawn path that read `state === "disposed" ? "disposed" : "starting"` — two lifetimes in one variable, which is what it was there to paper over. Verified before the change that nothing in the browser reads the wire string, so the value set is unchanged and unobserved either way; the four values it can take are the same four as before.
4
+
1
5
  ## 2.30.2
2
6
 
3
7
  - **Fix**: The cut-time shift of 2.28.0 is reverted — the field measured it and it moved the cuts OFF the source's keyframes rather than onto them. Of 75 pieces the picture produced afterwards, only **nine** began at a time the container's own table names, against **70 of 75** for the soundtrack, which the change never touched; the median distance from the playlist went from 0.04 s to 4.33 s. Before it, every piece began exactly on a named keyframe and it was the playlist that disagreed with them — which is the correction path's business, not the cut list's. The reasoning that produced the shift (that the muxer decides its cuts before the output is relabelled) was argued from ffmpeg's semantics rather than measured, and the measurement says otherwise.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.30.2",
3
+ "version": "2.31.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -25,7 +25,8 @@ import {
25
25
  ENCODE_RUN_STATE,
26
26
  INITIAL_RUN_STATE,
27
27
  nextState,
28
- processCanBeSignalled
28
+ processCanBeSignalled,
29
+ wireState
29
30
  } from "./encode-run-state.js";
30
31
  import { ENCODE_EXIT, classifyEncodeExit } from "./encode-exit.js";
31
32
 
@@ -1854,17 +1855,23 @@ export class HlsSessionManager {
1854
1855
  sourceMapKey,
1855
1856
  fileName: logName,
1856
1857
  dirPath: sessionDir,
1857
- state: "starting",
1858
+ // The SESSION's own lifetime, and nothing else: it exists, or it has been
1859
+ // disposed. It used to carry the encoder run's status as well, which is
1860
+ // why one line in the spawn path read `state === "disposed" ? "disposed"
1861
+ // : "starting"` — two lifetimes in one variable. The run's status lives
1862
+ // in `runState`.
1863
+ state: "live",
1858
1864
  startedAt: Date.now(),
1859
1865
  lastAccessedAt: Date.now(),
1860
1866
  ffmpeg: null,
1861
1867
  encodeRunGeneration: 0,
1862
1868
  // What the ENCODER RUN is doing, as one control state from the table in
1863
- // `encode-run-state.js`. Written at every event today and read by nothing
1864
- // yet: a refused pair in the log is the model disagreeing with reality,
1865
- // and that disagreement is the measurement this release exists to take.
1866
- // The fields it replaces`state`, `progress.state`
1867
- // and the repeated liveness checks keep their current writes meanwhile.
1869
+ // `encode-run-state.js`. Every question about the run is now answered
1870
+ // from here: whether a process can be signalled, whether anything is
1871
+ // reading the input, what a missing segment is answered with, and what
1872
+ // the browser is told. The three representations it replaceda status
1873
+ // string, a second status string on the wire, and a child-process handle
1874
+ // consulted at ten sites — could disagree with each other, and did.
1868
1875
  runState: INITIAL_RUN_STATE,
1869
1876
  lastError: "",
1870
1877
  // Cold-start timing: entry timestamp + a once-guard so the first servable
@@ -2007,7 +2014,10 @@ export class HlsSessionManager {
2007
2014
  seekFailureTarget: -1,
2008
2015
  seekFailureCount: 0,
2009
2016
  progress: {
2010
- state: "starting",
2017
+ // No `state` here. What the browser is told is `wireState(runState)`,
2018
+ // computed where it is sent — a Moore output rather than a field
2019
+ // maintained by hand at seven sites, which is how it came to be read
2020
+ // together with `session.state` under an `||`.
2011
2021
  processedSeconds: 0,
2012
2022
  startPositionSeconds: 0,
2013
2023
  totalSeconds: hasDuration ? durationSeconds : null,
@@ -2099,7 +2109,7 @@ export class HlsSessionManager {
2099
2109
  await this.waitUntilReady(session);
2100
2110
  return session;
2101
2111
  } catch (error) {
2102
- if (session.state === "failed") {
2112
+ if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
2103
2113
  await this.disposeSession(session.id);
2104
2114
  throw error;
2105
2115
  }
@@ -3253,7 +3263,7 @@ export class HlsSessionManager {
3253
3263
  if (
3254
3264
  !session ||
3255
3265
  session.state === "disposed" ||
3256
- session.state === "failed" ||
3266
+ session.runState === ENCODE_RUN_STATE.ENDED_FAILED ||
3257
3267
  !session.transcodeVideo ||
3258
3268
  // Nothing is encoding, so there is no speed to judge. A variant the
3259
3269
  // viewer has switched away from is left in exactly this state, and its
@@ -3812,8 +3822,6 @@ export class HlsSessionManager {
3812
3822
  session.encodeStartIndex = safeIndex;
3813
3823
  session.pendingRestartIndex = -1;
3814
3824
  session.lastRestartAt = Date.now();
3815
- session.state = session.state === "disposed" ? "disposed" : "starting";
3816
- session.progress.state = "running";
3817
3825
  session.progress.processedSeconds = startSeconds;
3818
3826
  session.progress.startPositionSeconds = startSeconds;
3819
3827
  session.progress.updatedAt = Date.now();
@@ -3912,7 +3920,6 @@ export class HlsSessionManager {
3912
3920
  } else if (key === "speed") {
3913
3921
  session.progress.speed = value;
3914
3922
  } else if (key === "progress") {
3915
- session.progress.state = value === "end" ? "ready" : "running";
3916
3923
  }
3917
3924
  const metrics = computeProgressMetrics(
3918
3925
  session.progress.processedSeconds,
@@ -3948,9 +3955,7 @@ export class HlsSessionManager {
3948
3955
  if (!this.#isCurrentRun(session, ffmpeg)) {
3949
3956
  return;
3950
3957
  }
3951
- session.state = "failed";
3952
3958
  session.lastError = error instanceof Error ? error.message : String(error);
3953
- session.progress.state = "failed";
3954
3959
  session.progress.updatedAt = Date.now();
3955
3960
  this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_FAILED);
3956
3961
  logger.error(`ffmpeg ${session.id} process error: ${session.lastError}`);
@@ -3989,9 +3994,7 @@ export class HlsSessionManager {
3989
3994
  // segment nobody was making. So the claim is checked against the
3990
3995
  // playlist we published, and a run that stopped short is a FAILURE that
3991
3996
  // can be restarted, not a finished file.
3992
- session.state = "failed";
3993
- session.progress.state = "failed";
3994
- session.progress.updatedAt = Date.now();
3997
+ session.progress.updatedAt = Date.now();
3995
3998
  session.lastError =
3996
3999
  `input ended after segment #${producedThrough} of ${expectedLast} — ` +
3997
4000
  "the source stopped delivering data";
@@ -4003,8 +4006,6 @@ export class HlsSessionManager {
4003
4006
  return;
4004
4007
  }
4005
4008
  if (outcome === ENCODE_EXIT.COMPLETE) {
4006
- session.state = "ready";
4007
- session.progress.state = "ready";
4008
4009
  session.progress.updatedAt = Date.now();
4009
4010
  this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_COMPLETE);
4010
4011
  logger.info(`transcode ${session.id} encode-run complete "${session.fileName}"`);
@@ -4066,11 +4067,9 @@ export class HlsSessionManager {
4066
4067
  // stays for what it was built for, a target that genuinely cannot be
4067
4068
  // encoded; it must not condemn a session whose data merely went away.
4068
4069
  if (outcome === ENCODE_EXIT.INPUT_LOST) {
4069
- session.state = "recovering";
4070
4070
  // On the wire it is simply "not ready yet" — a state the browser has
4071
4071
  // always known how to wait through. Only the proxy needs the
4072
4072
  // distinction between waiting for data and having given up.
4073
- session.progress.state = "starting";
4074
4073
  session.progress.updatedAt = Date.now();
4075
4074
  this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_INPUT_LOST);
4076
4075
  session.inputRetryCount = (session.inputRetryCount ?? 0) + 1;
@@ -4085,7 +4084,7 @@ export class HlsSessionManager {
4085
4084
  );
4086
4085
  session.inputRetryTimer = setTimeout(() => {
4087
4086
  session.inputRetryTimer = null;
4088
- if (session.state !== "recovering") {
4087
+ if (session.runState !== ENCODE_RUN_STATE.RETRY_WAIT) {
4089
4088
  return;
4090
4089
  }
4091
4090
  this.#transitionRun(session, ENCODE_RUN_EVENT.RETRY_DUE);
@@ -4097,8 +4096,6 @@ export class HlsSessionManager {
4097
4096
  session.inputRetryTimer.unref?.();
4098
4097
  return;
4099
4098
  }
4100
- session.state = "failed";
4101
- session.progress.state = "failed";
4102
4099
  session.progress.updatedAt = Date.now();
4103
4100
  this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_FAILED);
4104
4101
  logger.error(
@@ -4579,10 +4576,9 @@ export class HlsSessionManager {
4579
4576
  // Individual segments are long-polled by the segment route as ffmpeg
4580
4577
  // produces them.
4581
4578
  if (session.useSyntheticPlaylist) {
4582
- if (session.state === "failed") {
4579
+ if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
4583
4580
  throw new Error(session.lastError || "ffmpeg failed to start HLS session.");
4584
4581
  }
4585
- session.state = "ready";
4586
4582
  return;
4587
4583
  }
4588
4584
 
@@ -4590,15 +4586,14 @@ export class HlsSessionManager {
4590
4586
  const deadline = Date.now() + this.startupWaitMs;
4591
4587
 
4592
4588
  while (Date.now() < deadline) {
4593
- if (session.state === "failed") {
4589
+ if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
4594
4590
  throw new Error(session.lastError || "ffmpeg failed to start HLS session.");
4595
4591
  }
4596
4592
  try {
4597
4593
  await access(playlistPath);
4598
4594
  const text = await readFile(playlistPath, "utf8");
4599
4595
  if (text.includes("#EXTM3U")) {
4600
- session.state = "ready";
4601
- return;
4596
+ return;
4602
4597
  }
4603
4598
  } catch (_error) {
4604
4599
  // Playlist is not ready yet.
@@ -5114,7 +5109,14 @@ export class HlsSessionManager {
5114
5109
  const median = deviations.length > 0 ? deviations[Math.floor(deviations.length / 2)] : 0;
5115
5110
  const landed = check.landedOnAnotherKeyframe ?? 0;
5116
5111
  logger.info(
5117
- `keyframe-index ${session.containerFormat || "unknown"} "${session.fileName}": ` +
5112
+ // The session id, because without it this line cannot be attributed. A
5113
+ // family produces one summary per member — the picture and each
5114
+ // soundtrack — and on 2026-08-17 the picture's was read as the sound's,
5115
+ // from a neighbouring log line, and a roadmap item was written against
5116
+ // the wrong half of the stream. The id is the only thing that says whose
5117
+ // reading this is.
5118
+ `keyframe-index ${session.id.slice(0, 8)} ${session.audioOnly === true ? "sound" : "picture"} ` +
5119
+ `${session.containerFormat || "unknown"} "${session.fileName}": ` +
5118
5120
  `${check.disagreed} of ${check.checked} produced segments started away from the playlist, ` +
5119
5121
  `median ${median.toFixed(3)}s worst ${check.maxDeviationSec.toFixed(3)}s` +
5120
5122
  (check.firstDisagreementIndex >= 0 ? ` (first at #${check.firstDisagreementIndex})` : "") +
@@ -5374,7 +5376,7 @@ export class HlsSessionManager {
5374
5376
  if (
5375
5377
  !session ||
5376
5378
  session.state === "disposed" ||
5377
- session.state === "failed" ||
5379
+ session.runState === ENCODE_RUN_STATE.ENDED_FAILED ||
5378
5380
  !session.ffmpeg ||
5379
5381
  session.runState === ENCODE_RUN_STATE.SUSPENDED
5380
5382
  ) {
@@ -6923,13 +6925,13 @@ export class HlsSessionManager {
6923
6925
  if (!session || !isSafeFileName(fileName, session.segmentFormat)) {
6924
6926
  return { kind: "not-found" };
6925
6927
  }
6926
- if (session.state === "recovering") {
6928
+ if (session.runState === ENCODE_RUN_STATE.RETRY_WAIT) {
6927
6929
  // The data went away and is being fetched again. Holding the request is
6928
6930
  // the truthful answer: nothing is broken and there is nothing for the
6929
6931
  // viewer to retry.
6930
6932
  return { kind: "warming-up" };
6931
6933
  }
6932
- if (session.state === "failed") {
6934
+ if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
6933
6935
  return {
6934
6936
  kind: "failed",
6935
6937
  message: session.lastError || "ffmpeg failed for this transcode session."
@@ -7496,7 +7498,11 @@ export class HlsSessionManager {
7496
7498
  session.lastAccessedAt = Date.now();
7497
7499
  const warmupTotalSeconds = this.startupWaitMs / 1000;
7498
7500
  const warmupElapsedSeconds = Math.max(0, (Date.now() - session.startedAt) / 1000);
7499
- const isWarmupPhase = session.state === "starting" || session.progress.state === "starting";
7501
+ // One question, one answer. This used to read BOTH strings with `||`
7502
+ // because neither could answer alone: `session.state` said "starting" from
7503
+ // the first spawn until something else overwrote it, and `progress.state`
7504
+ // said it again on its own schedule.
7505
+ const isWarmupPhase = wireState(session.runState) === "starting";
7500
7506
  const warmupPercent = isWarmupPhase
7501
7507
  ? Math.max(0, Math.min(100, (warmupElapsedSeconds / warmupTotalSeconds) * 100))
7502
7508
  : null;
@@ -7514,7 +7520,7 @@ export class HlsSessionManager {
7514
7520
  // The id the caller asked about, not the variant it was answered from —
7515
7521
  // the browser tracks its sessions by the id it was given.
7516
7522
  sessionId: named.id,
7517
- state: session.progress.state,
7523
+ state: wireState(session.runState),
7518
7524
  processedSeconds: session.progress.processedSeconds,
7519
7525
  startPositionSeconds: session.progress.startPositionSeconds ?? 0,
7520
7526
  totalSeconds: session.progress.totalSeconds,
@@ -7554,7 +7560,7 @@ export class HlsSessionManager {
7554
7560
  expectedSessionCreateMs: this.expectedSessionCreateMs(),
7555
7561
  expectedFirstSegmentMs: this.expectedFirstSegmentMs(),
7556
7562
  updatedAt: session.progress.updatedAt,
7557
- error: session.state === "failed" ? session.lastError : ""
7563
+ error: session.runState === ENCODE_RUN_STATE.ENDED_FAILED ? session.lastError : ""
7558
7564
  };
7559
7565
  }
7560
7566