@torrent-tv/proxy 2.59.1 → 2.59.3

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,12 +1,21 @@
1
- ## 2.59.0
1
+ ## 2.59.3
2
2
 
3
- - **New**: a wedge (roadmap item 11) is now declared, and evidence gathered for it automatically, even when the small-message shape means nothing ever queues. The only trigger that existed (`wedgeIsCertain`) requires a nonzero channel queue confirmed 2026-08-28 by reading the code, not assuming it from the note: `bufferedAmount()` reads 0 the instant our bytes reach usrsctp, whatever usrsctp does with them next, so a wedge carrying only probes and control messages never set it and the last real episode's ring was never saved automatically. The delivery probe already computes a queue-independent verdict (`association-stopped`); it is now wired to the same evidence-gathering, gated on its OWN certainty rule rather than the raw verdict a connection can sit behind by a bounded, non-growing amount for minutes (measured the same day, a backgrounded tab: gap held at 6-7 probes for 95+ seconds while `seen` kept climbing right along with `sent`) without anything being wrong. What a true wedge shows instead, checked against a session already known to be one: `seen` frozen at one value for over a minute while `sent` climbs unbounded. `probeWedgeIsCertain` asks whether the counter has stopped moving for longer than this connection's own history says a healthy gap ever takes the same shape `wedgeIsCertain` already uses, applied to the probe's own counter.
4
- - **New**: `usrsctp`'s live association state (peer receive window, pending data, retransmission timeout, congestion window) can now be read on either wedge declaration, automatically `node_datachannel.node` ships unstripped, so the read is a gdb attach against the running process, no rebuild. The walk and its healthy baseline are `research/session-2026-08-27-28-freeze-onset-and-sessions.md`, section 1; the script that performs it ships in the package (`assets/diagnostics/sctpstate.gdb`) instead of surviving only as long as someone remembers to copy it back onto a host after a container is recreated. Nine episodes of this item have lacked exactly this reading.
3
+ - **Fix**: two quality rungs that this machine encodes at the same size now share one encoder instead of starting one each. A variant was remembered under the height the browser ASKED for, while what it encodes is settled afterwards by the clamp that starts a manual pick at the top of the ladder this host can sustain so on a weak machine a request for 360p and one for 540p both became a 426x240 encode, were filed under keys 360 and 540, and neither ever found the 240p session already making that exact picture. Field 2026-08-28: three ffmpeg processes on a CM4 producing one identical picture, every rung above 240p then measured at 0.04x of realtime and 240p itself at 0.30-0.72x, the viewer watching a slideshow that ended in a spinner, and the process dying eight minutes later after resident memory grew 121→810 MB. The comparison is made on the height PRODUCED, which is the only figure that cannot be wrong: predicting the clamp instead would be a second copy of the budget arithmetic, and the two would drift the offer prices a rung from the startup measurement while the clamp prices it from what this file has since been seen to cost. A duplicate is let go the moment its size is known, and the height it was asked for then names the session that serves it, so no later request starts anything. A COPY is never adopted for a re-encoded rung: it costs no encoder and is the one thing a stranded viewer can always return to (`research/session-pileup-variant-key-2026-08-28.md`).
4
+ - **Fix**: disposing a variant clears every height that named it, not only the one it was created under. With one session now serving several requested heights, the old cleanup left the other keys pointing at a disposed session recovered on the next request, but only after it had been found dead.
5
+
6
+ ## 2.59.2
7
+
8
+ - **New**: a failed encode run says what it asked the source for and what the source holds, beside the command it ran. Every `-map` this proxy builds carries the `?` suffix, so ffmpeg drops a mapping for an absent stream in silence; drop them all and the output has no streams, which it reports as `Output file does not contain any stream` and exit 255. Three sessions died that way on 2026-08-26 and the log held only the code — indistinguishable from any other refusal. The stream counts are read from the probe's own banner, which is the one place the source says what it carries, and kept for the failure to quote. Beside them the cause is unmistakable and it says which side is wrong: an audio index past the end of the list is ours, no streams at all is the source's. The argument list was already logged at spawn; it is now repeated at the failure, so it need not be found among the runs that succeeded around it (roadmap item 58, `research/session-pileup-variant-key-2026-08-28.md`).
5
9
 
6
10
  ## 2.59.1
7
11
 
8
12
  - **Fix**: the usrsctp state script no longer crashes when no association exists yet. Found while verifying 2.59.0 by hand against the live process with no viewer connected: `system_base_info`'s association hash table is a null pointer until usrsctp allocates its first association, and the walk dereferenced it unconditionally — `Cannot access memory at address 0x0` instead of the graceful "no association found" the script already prints for the equivalent case where the table exists but is empty. Guarded the walk behind `$hash != 0`.
9
13
 
14
+ ## 2.59.0
15
+
16
+ - **New**: a wedge (roadmap item 11) is now declared, and evidence gathered for it automatically, even when the small-message shape means nothing ever queues. The only trigger that existed (`wedgeIsCertain`) requires a nonzero channel queue — confirmed 2026-08-28 by reading the code, not assuming it from the note: `bufferedAmount()` reads 0 the instant our bytes reach usrsctp, whatever usrsctp does with them next, so a wedge carrying only probes and control messages never set it and the last real episode's ring was never saved automatically. The delivery probe already computes a queue-independent verdict (`association-stopped`); it is now wired to the same evidence-gathering, gated on its OWN certainty rule rather than the raw verdict — a connection can sit behind by a bounded, non-growing amount for minutes (measured the same day, a backgrounded tab: gap held at 6-7 probes for 95+ seconds while `seen` kept climbing right along with `sent`) without anything being wrong. What a true wedge shows instead, checked against a session already known to be one: `seen` frozen at one value for over a minute while `sent` climbs unbounded. `probeWedgeIsCertain` asks whether the counter has stopped moving for longer than this connection's own history says a healthy gap ever takes — the same shape `wedgeIsCertain` already uses, applied to the probe's own counter.
17
+ - **New**: `usrsctp`'s live association state (peer receive window, pending data, retransmission timeout, congestion window) can now be read on either wedge declaration, automatically — `node_datachannel.node` ships unstripped, so the read is a gdb attach against the running process, no rebuild. The walk and its healthy baseline are `research/session-2026-08-27-28-freeze-onset-and-sessions.md`, section 1; the script that performs it ships in the package (`assets/diagnostics/sctpstate.gdb`) instead of surviving only as long as someone remembers to copy it back onto a host after a container is recreated. Nine episodes of this item have lacked exactly this reading.
18
+
10
19
  ## 2.58.3
11
20
 
12
21
  - **New**: the proxy says what it is holding, once a minute — resident memory, heap, external and array buffers, the torrent stores in BYTES, and what the machine has left. It was killed on 2026-08-28 by the kernel's own out-of-memory killer at 2.4 GB resident (`exit code 137`, no core dump, `Out of memory: Killed process ... anon-rss: 2422628kB`) and the log had never recorded a single figure about memory. There was one final reading, taken by the kernel, and no series leading to it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.59.1",
3
+ "version": "2.59.3",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -119,6 +119,54 @@ export function parseFfmpegVideoDimensions(stderrText) {
119
119
  };
120
120
  }
121
121
 
122
+ /**
123
+ * Count the streams the source declares, by type, from ffmpeg's banner
124
+ * ("Stream #0:3(rus): Audio: ac3 …").
125
+ *
126
+ * Every `-map` this proxy builds carries the `?` suffix, which tells ffmpeg to
127
+ * drop the mapping silently when the stream is absent instead of refusing. Map
128
+ * every stream of a run away that way and the output is left with nothing in
129
+ * it, which ffmpeg reports as `Output file does not contain any stream` and
130
+ * exit 255 — three sessions died that way on 2026-08-26 and the log held only
131
+ * the exit code. What separates "we asked for a track that is not there" from
132
+ * every other cause of 255 is this count, so it is read once, with the rest of
133
+ * the banner, and kept for the failure to quote.
134
+ *
135
+ * @param {string} stderrText
136
+ * @returns {{ video: number, audio: number, subtitle: number, other: number } | null}
137
+ * Null when the banner carried no stream lines at all — which is itself
138
+ * different from a file that genuinely has none.
139
+ */
140
+ export function parseFfmpegStreamCounts(stderrText) {
141
+ if (typeof stderrText !== "string" || stderrText.length === 0) {
142
+ return null;
143
+ }
144
+ const lines = stderrText.match(/^\s*Stream #\d+:\d+.*$/gim);
145
+ if (!lines || lines.length === 0) {
146
+ return null;
147
+ }
148
+ const counts = { video: 0, audio: 0, subtitle: 0, other: 0 };
149
+ for (const line of lines) {
150
+ // The type follows the stream's id and its optional language/metadata:
151
+ // "Stream #0:1(eng): Audio: aac". Attached pictures also announce
152
+ // themselves as Video, and they are counted as such deliberately — that is
153
+ // exactly what `0:v:0?` would map, and a cover image mapped as the picture
154
+ // is one of the ways a run ends up producing nothing anyone can watch.
155
+ const match = line.match(/Stream #\d+:\d+(?:\[[^\]]*\])?(?:\([^)]*\))?:\s*(\w+)/i);
156
+ const kind = match ? match[1].toLowerCase() : "";
157
+ if (kind === "video") {
158
+ counts.video += 1;
159
+ } else if (kind === "audio") {
160
+ counts.audio += 1;
161
+ } else if (kind === "subtitle") {
162
+ counts.subtitle += 1;
163
+ } else {
164
+ counts.other += 1;
165
+ }
166
+ }
167
+ return counts;
168
+ }
169
+
122
170
  /**
123
171
  * Parse the source frame rate from the ffmpeg "Video:" line
124
172
  * (e.g. "… 23.98 fps," / "… 25 fps,"). Returns null when absent.