@torrent-tv/proxy 2.30.1 → 2.30.2

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,8 @@
1
+ ## 2.30.2
2
+
3
+ - **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.
4
+ - **Fix**: The steering line compared two different things. `steered onto N of M holders` summed the successes over every attempt of a wait while taking M from the last attempt alone, which is how the log came to read `steered onto 12 of 6 holders`. Both halves are now totals over the same attempts: `steered onto N of M asks (K peers held it)`.
5
+
1
6
  ## 2.30.1
2
7
 
3
8
  - **Fix**: A seek was undone a second after it was made. Measured 2026-08-17: the viewer jumped to 2083.4 s, both runs restarted at segment #373 — correctly — and then a request for #371, issued by the player BEFORE the jump and reissued a second later, dragged the encoder back to #370. The viewer sat at #374 waiting for it to return. Two things let that happen, and both are fixed. The behind-head repair refuses a request that is behind the position the VIEWER themselves reported: its existing guard only holds while a seek is still settling, which by then it was not. And a segment request may no longer move the recorded viewer position BACKWARDS past a reported seek — playback only ever moves forward from one, so nothing legitimate is lost, while a stale request can no longer rewrite the viewer's own statement, which is how the repair came to believe it. A reported seek is the viewer stating where they are; a request is evidence about where the player is reading, and evidence may refine a statement forward, never contradict it backwards. Pinned by `test/stale-request-after-seek.test.js`, whose control case shows the same traffic still repairing a genuinely misplaced run when the viewer has said nothing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.30.1",
3
+ "version": "2.30.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -3567,30 +3567,22 @@ export class HlsSessionManager {
3567
3567
  const gridCutTimes = explicitTimes && (!session.transcodeVideo || session.cutGrid === "keyframe")
3568
3568
  ? segmentCutTimesFrom(session.segmentBoundaries, safeIndex)
3569
3569
  : null;
3570
- // On the COPY branch the muxer decides its cuts against the source's own
3571
- // timestamps, not against the labels we ask it to write. That branch keeps
3572
- // the source's timestamps (`-copyts`) and re-labels the output 0-based with
3573
- // `-output_ts_offset -sourceStartTime`; the cut list, being applied before
3574
- // that relabelling, must therefore be stated in the SOURCE's terms.
3570
+ // Cut times are stated on the grid, for both branches.
3575
3571
  //
3576
- // Measured 2026-08-17, and this is the whole of the trouble: asked to cut
3577
- // at 808.808 s on the 0-based grid, ffmpeg cut at 806.806 s — exactly
3578
- // `sourceStartTime` (2.002 s) early, and 806.806 s is itself a keyframe the
3579
- // container's table names, which is why every "disagreement" landed on
3580
- // another real keyframe. The soundtrack, which is re-encoded and takes the
3581
- // other branch, cut where it was asked. The two then told the shared
3582
- // boundary table different things and corrected each other back and forth
3583
- // for the whole session (#202: 808.808 806.806 808.750 → …), so the
3584
- // playlist and the media drifted apart by a whole segment and the player
3585
- // refetched what it could not place.
3572
+ // 2.28.0 added `sourceStartTime` to them on the copy branch, reasoning that
3573
+ // the muxer decides its cuts before the output is relabelled. The field
3574
+ // measured it the next session and the reasoning was wrong: of 75 pieces
3575
+ // the picture produced, only NINE began at a time the container's own
3576
+ // keyframe table names (the soundtrack, untouched by the change, scored 70
3577
+ // of 75). Before it, every piece began exactly on a named keyframe and it
3578
+ // was the PLAYLIST that disagreed with them. So the shift moved the cuts
3579
+ // OFF the keyframes rather than onto them, and it is gone.
3586
3580
  //
3587
- // Nothing here is a guess about ffmpeg's semantics: the shift is the same
3588
- // one the seek already applies on this branch (`seekSeconds = startSeconds
3589
- // + sourceStartTime`), and the field measurement above is what says the
3590
- // cuts needed it too.
3591
- const cutTimes = gridCutTimes && onKeyframeGridFor(session) && sourceStartTime !== 0
3592
- ? gridCutTimes.map((time) => Number((time + sourceStartTime).toFixed(6)))
3593
- : gridCutTimes;
3581
+ // What remains true, and is what that measurement is really about: the
3582
+ // picture cuts where the source's keyframes are, and the playlist must be
3583
+ // built from those same times. That is the correction path's job, not the
3584
+ // cut list's.
3585
+ const cutTimes = gridCutTimes;
3594
3586
 
3595
3587
  // A second chance for a predecessor that survived the escalation above —
3596
3588
  // the first block is the one that does the work. Its exit is ignored
@@ -87,7 +87,7 @@ function speedOf(wire) {
87
87
  * @param {object} torrent
88
88
  * @param {number} pieceIndex
89
89
  * @param {number} [limit] - How many wires to push it onto.
90
- * @returns {{ asked: number, considered: number, fastestBytesPerSecond: number }}
90
+ * @returns {{ asked: number, attempted: number, considered: number, fastestBytesPerSecond: number }}
91
91
  * `asked` counts requests the library actually placed: it refuses when a
92
92
  * wire's pipeline is full or when nothing can be reserved even with hotswap,
93
93
  * and that refusal is information — a piece nobody can be asked for is
@@ -95,7 +95,7 @@ function speedOf(wire) {
95
95
  */
96
96
  export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
97
97
  if (!canPlaceRequests(torrent) || !Number.isInteger(pieceIndex) || pieceIndex < 0) {
98
- return { asked: 0, considered: 0, fastestBytesPerSecond: 0 };
98
+ return { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
99
99
  }
100
100
  const candidates = wiresForPiece(torrent, pieceIndex);
101
101
  let asked = 0;
@@ -116,6 +116,13 @@ export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
116
116
  return {
117
117
  asked,
118
118
  considered: candidates.length,
119
+ // How many of the asks the library placed, against how many it was asked
120
+ // for. The caller sums these over the whole wait, and summing `asked`
121
+ // against a `considered` taken from the LAST attempt is how the field log
122
+ // came to read "steered onto 12 of 6 holders" — a ratio of two different
123
+ // things. Both halves are returned per attempt so the caller can add each
124
+ // to its own total.
125
+ attempted: Math.min(candidates.length, Math.max(1, limit)),
119
126
  fastestBytesPerSecond: candidates.length > 0 ? speedOf(candidates[0]) : 0
120
127
  };
121
128
  }
@@ -517,12 +517,15 @@ export async function* readFragments({
517
517
  // holder delivers — measured 2026-08-17, the swarm had a fivefold surplus
518
518
  // of bandwidth and the reader still waited 1.0-4.5 s, 47 times in two
519
519
  // minutes, on pieces five peers already had.
520
- let pushed = { asked: 0, considered: 0, fastestBytesPerSecond: 0 };
520
+ let pushed = { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
521
521
  const pushToFastest = () => {
522
522
  try {
523
523
  const result = askFastestWiresFor(torrent, pieceIndex);
524
524
  pushed = {
525
525
  asked: pushed.asked + result.asked,
526
+ // Summed like the successes, so the line compares two totals over
527
+ // the same attempts instead of a total against a snapshot.
528
+ attempted: (pushed.attempted ?? 0) + result.attempted,
526
529
  considered: result.considered,
527
530
  fastestBytesPerSecond: result.fastestBytesPerSecond
528
531
  };
@@ -592,7 +595,7 @@ export async function* readFragments({
592
595
  // What WE did about it, so the next session says whether steering
593
596
  // the piece onto faster holders shortens the tail — by number
594
597
  // rather than by impression.
595
- `; steered onto ${pushed.asked} of ${pushed.considered} holders` +
598
+ `; steered onto ${pushed.asked} of ${pushed.attempted} asks (${pushed.considered} peers held it)` +
596
599
  (pushed.fastestBytesPerSecond > 0
597
600
  ? `, fastest ${Math.round(pushed.fastestBytesPerSecond / 1024)}KB/s`
598
601
  : "")
@@ -97,7 +97,7 @@ test("a build without the request entry is reported, not silently skipped", () =
97
97
  assert.equal(canPlaceRequests({ wires: [] }), false);
98
98
  assert.equal(canPlaceRequests({ wires: [], _request: () => true }), true);
99
99
  const result = askFastestWiresFor({ wires: [wire({ speed: 1 })] }, 1);
100
- assert.deepEqual(result, { asked: 0, considered: 0, fastestBytesPerSecond: 0 });
100
+ assert.deepEqual(result, { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 });
101
101
  });
102
102
 
103
103
  test("a wire that cannot say how fast it is ranks last rather than throwing", () => {