@torrent-tv/proxy 2.14.1 → 2.14.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 +4 -0
- package/package.json +1 -1
- package/services/hls-session-manager.js +20 -41
- package/test/quality-variants.test.js +28 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2.14.2
|
|
2
|
+
|
|
3
|
+
- **Fix**: A separately published audio track starts BEHIND the picture's read head, and a request behind its run is answered as before. Two mistakes compounded in 2.14.1 and left the viewer on a spinner that never ended. The position this class keeps is written by the segments a session serves — the READ head — while the viewer's picture sits behind it by everything they have buffered, so the track was started AHEAD of them: field 2026-08-15, the run placed at segment #16 while the player asked for #10. On top of that, 2.14.1 had begun answering such a request "not found" at once instead of holding it, which turned a condition the encoder used to correct in twenty seconds into a permanent refusal: hls.js retried #10 for a minute and a half, raised a fatal network error, recovered, and retried it again. The prompt refusal is withdrawn — it was written for a probe and met a real request — and the track now starts a whole look-ahead behind the read head, which is exactly how far apart the two can be. The price is audio the player already holds: at ten to twenty times realtime and 75 KB a piece, a second or two of work.
|
|
4
|
+
|
|
1
5
|
## 2.14.1
|
|
2
6
|
|
|
3
7
|
- **Fix**: Changing the audio track no longer costs twenty seconds of silence. The player asks the NEW rendition for its segment #0 before anything else — measured 2026-08-15, a track changed at 159 s with the rendition correctly placed at #26 — and the repair that exists for a run placed WRONGLY took that literally: it killed the run and restarted the encoder at the beginning of the film, so the segment the viewer was waiting for arrived 20.5 s later. A rendition is never repaired by moving it, because its run is placed where the viewer is and the request behind it is the player probing; and such a request is now answered at once rather than held, since holding it spends the player's patience on a fragment that can never be produced. The refusal stands down while a seek of the rendition's own is settling: a viewer going BACKWARDS is reported to the base and forwarded to the rendition, but its run only moves when the settle fires, so until then the requests for the new position are behind the old one — and those are exactly the ones the viewer is waiting for.
|
package/package.json
CHANGED
|
@@ -3570,17 +3570,6 @@ export class HlsSessionManager {
|
|
|
3570
3570
|
if (head - index > BEHIND_HEAD_REPAIR_MAX_SEGMENTS) {
|
|
3571
3571
|
return;
|
|
3572
3572
|
}
|
|
3573
|
-
// An audio rendition is never repaired by moving it. Its run is placed
|
|
3574
|
-
// where the VIEWER is, from the session they are watching, so a request
|
|
3575
|
-
// behind that is not a run in the wrong place — it is the player probing.
|
|
3576
|
-
// Measured 2026-08-15: changing track at 159 s, hls.js asked for the new
|
|
3577
|
-
// rendition's segment #0, the repair obligingly took the encoder to the
|
|
3578
|
-
// start of the film, and the audio the viewer was waiting for arrived
|
|
3579
|
-
// 20.5 s later instead of at once. Left where it is, the right segment is
|
|
3580
|
-
// already being produced.
|
|
3581
|
-
if (session.audioOnly === true) {
|
|
3582
|
-
return;
|
|
3583
|
-
}
|
|
3584
3573
|
// Nothing is encoding: a rung the viewer has switched away from is left
|
|
3585
3574
|
// exactly so, and its held requests must not bring its encoder back.
|
|
3586
3575
|
if (session.ffmpeg == null || hasChildExited(session.ffmpeg)) {
|
|
@@ -5345,8 +5334,26 @@ export class HlsSessionManager {
|
|
|
5345
5334
|
// while the player is asking for segment #537 — and the audio would begin
|
|
5346
5335
|
// at zero and never catch up, since nothing treats a far request as a
|
|
5347
5336
|
// seek. The accessor falls back to the last segment actually requested.
|
|
5348
|
-
|
|
5349
|
-
|
|
5337
|
+
// Behind where the picture has been READ to, by the whole look-ahead.
|
|
5338
|
+
//
|
|
5339
|
+
// The position this class keeps is written by the segments a session
|
|
5340
|
+
// serves, so it is the READ head, and the player's picture sits behind it
|
|
5341
|
+
// by everything it has buffered — up to the look-ahead cap. Starting the
|
|
5342
|
+
// audio at the read head therefore starts it AHEAD of the viewer, and
|
|
5343
|
+
// every request they then make is behind a run that only moves forward.
|
|
5344
|
+
// Field 2026-08-15: the run was placed at #16, the player asked for #10,
|
|
5345
|
+
// and the audio never arrived until the encoder was dragged back.
|
|
5346
|
+
//
|
|
5347
|
+
// The cap is exactly how far apart the two can be, so subtracting it
|
|
5348
|
+
// cannot leave the run ahead of the viewer. The price is audio the player
|
|
5349
|
+
// already has — for a track being encoded at ten to twenty times realtime
|
|
5350
|
+
// and served in 75 KB pieces, that is a second or two of work.
|
|
5351
|
+
startPositionSeconds: Math.max(
|
|
5352
|
+
0,
|
|
5353
|
+
Math.floor(
|
|
5354
|
+
(this.#viewerPositionOf(this.#activeVariant(base)) - LOOKAHEAD_PAUSE_SECONDS) / 10
|
|
5355
|
+
) * 10
|
|
5356
|
+
),
|
|
5350
5357
|
segmentFormatId: base.segmentFormat.id,
|
|
5351
5358
|
// Cut where the picture is cut. Two streams meant to be played together
|
|
5352
5359
|
// have to be divided at the same times, and the grid is the base's — the
|
|
@@ -5874,34 +5881,6 @@ export class HlsSessionManager {
|
|
|
5874
5881
|
// at this position (server-side seeking). The caller long-polls.
|
|
5875
5882
|
if (!isPlaylist) {
|
|
5876
5883
|
const requestedIndex = session.segmentFormat.segmentIndexFromName(fileName);
|
|
5877
|
-
// An audio rendition asked for something behind its run says so at once
|
|
5878
|
-
// instead of holding. Its run is placed where the viewer is and is not
|
|
5879
|
-
// moved from there, so the request can never be answered — and holding it
|
|
5880
|
-
// for the full window costs the player its own patience on the fragment
|
|
5881
|
-
// it needs NEXT. Measured 2026-08-15: on a track change at 159 s hls.js
|
|
5882
|
-
// asked for segment #0, and a held request plus a repaired encoder cost
|
|
5883
|
-
// 20.5 s of silence. Refused promptly, the player moves to the segment
|
|
5884
|
-
// that is genuinely being produced.
|
|
5885
|
-
if (
|
|
5886
|
-
session.audioOnly === true &&
|
|
5887
|
-
Number.isFinite(requestedIndex) &&
|
|
5888
|
-
requestedIndex < (session.encodeStartIndex ?? 0) &&
|
|
5889
|
-
session.ffmpeg != null &&
|
|
5890
|
-
// Not while a seek of its own is settling. A viewer seeking BACKWARDS
|
|
5891
|
-
// is reported to the base and forwarded here, but the run only moves
|
|
5892
|
-
// when the settle fires — until then `encodeStartIndex` still names the
|
|
5893
|
-
// old position, and every request for the new one is "behind" it. Those
|
|
5894
|
-
// are exactly the requests the viewer is waiting for, so they are held,
|
|
5895
|
-
// as they were before this refusal existed.
|
|
5896
|
-
session.seekTarget == null &&
|
|
5897
|
-
session.seekSettleTimer == null
|
|
5898
|
-
) {
|
|
5899
|
-
logger.info(
|
|
5900
|
-
`transcode ${session.id} audio segment #${requestedIndex} is behind this rendition's run ` +
|
|
5901
|
-
`(#${session.encodeStartIndex}); it is not made and the run stays where the viewer is`
|
|
5902
|
-
);
|
|
5903
|
-
return { kind: "not-found" };
|
|
5904
|
-
}
|
|
5905
5884
|
this.#ensureEncodingFor(
|
|
5906
5885
|
session,
|
|
5907
5886
|
requestedIndex,
|
|
@@ -638,46 +638,38 @@ test("a rung served by copy stays offered while a re-encoded rung is on screen",
|
|
|
638
638
|
);
|
|
639
639
|
});
|
|
640
640
|
|
|
641
|
-
test("
|
|
642
|
-
const { manager, dirPath } = await managerWithBase();
|
|
641
|
+
test("a separately published audio track starts behind the picture's read head", async (t) => {
|
|
642
|
+
const { manager, base, dirPath } = await managerWithBase();
|
|
643
643
|
t.after(async () => {
|
|
644
644
|
await manager.disposeAll();
|
|
645
645
|
await rm(dirPath, { recursive: true, force: true });
|
|
646
646
|
});
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
answer.kind,
|
|
666
|
-
"not-found",
|
|
667
|
-
"answered at once: the run stays where the viewer is, so this can never be produced"
|
|
668
|
-
);
|
|
669
|
-
assert.equal(rendition.encodeStartIndex, 26, "and the encoder was not moved to the start of the film");
|
|
670
|
-
assert.equal(rendition.seekTarget ?? null, null, "no seek was armed for it either");
|
|
671
|
-
|
|
672
|
-
// A seek of its own is the case where "behind the run" is temporary and real:
|
|
673
|
-
// the viewer went backwards, the base forwarded it here, and the run moves
|
|
674
|
-
// when the settle fires. Until then the request is held, not refused — it is
|
|
675
|
-
// the one the viewer is waiting for.
|
|
676
|
-
rendition.seekTarget = 4;
|
|
677
|
-
rendition.seekSettleTimer = setTimeout(() => {}, 60_000);
|
|
678
|
-
t.after(() => clearTimeout(rendition.seekSettleTimer));
|
|
647
|
+
// What broke playback on 2026-08-15: the position this class keeps is where
|
|
648
|
+
// segments have been SERVED to, and the viewer's picture is behind it by
|
|
649
|
+
// everything they have buffered. Started at the read head, the audio run sat
|
|
650
|
+
// ahead of the viewer and every request they made was behind a run that only
|
|
651
|
+
// moves forward.
|
|
652
|
+
base.viewerPositionSeconds = 140;
|
|
653
|
+
base.audioSeparate = true;
|
|
654
|
+
manager.getCachedAudioTracks = () => [
|
|
655
|
+
{ index: 0, language: "rus", title: "", isDefault: true },
|
|
656
|
+
{ index: 1, language: "eng", title: "", isDefault: false }
|
|
657
|
+
];
|
|
658
|
+
const created = [];
|
|
659
|
+
manager.createOrGetSession = async (params) => {
|
|
660
|
+
created.push(params);
|
|
661
|
+
const rendition = fakeSession({ id: VARIANT_ID, encodeHeight: 0, dirPath });
|
|
662
|
+
rendition.audioOnly = true;
|
|
663
|
+
return { sessionId: VARIANT_ID, session: rendition };
|
|
664
|
+
};
|
|
679
665
|
|
|
680
|
-
|
|
666
|
+
// A segment, not the playlist: the playlist is answered from the base and
|
|
667
|
+
// deliberately starts no encoder.
|
|
668
|
+
await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4");
|
|
681
669
|
|
|
682
|
-
assert.equal(
|
|
670
|
+
assert.equal(created.length, 1, "the track's own session was made");
|
|
671
|
+
assert.ok(
|
|
672
|
+
created[0].startPositionSeconds <= 20,
|
|
673
|
+
`started behind the picture, not at the read head — got ${created[0].startPositionSeconds}s for a read head of 140s`
|
|
674
|
+
);
|
|
683
675
|
});
|