@torrent-tv/proxy 2.26.0 → 2.27.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.27.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: Picture and sound now begin a run at the same instant. They were asked for the same time and landed in different places: a copied picture may begin only at a real keyframe and may not begin before the time asked for — that content belongs to the previous segment — so it moves FORWARD to the next keyframe, by up to the keyframe spacing (0.58-2.96 s measured on the field file); a soundtrack has no keyframes and begins exactly where asked, to within one audio frame. So after every seek the two runs of one film began up to three seconds apart. The picture's true start is measured from the piece it produces, and that measurement now moves every other member of the family whose run begins at the same boundary. Restarted at the boundary rather than seeked to the time, deliberately: a seek decides by segment index, finds the run already begins there and answers "already within the running encode" — true about the index and false about the instant, which is why the first version of this fix moved nothing at all.
|
|
4
|
+
|
|
1
5
|
## 2.26.0
|
|
2
6
|
|
|
3
7
|
- **New**: The keyframe-index measurement now answers the question it was raising. Each file's summary reports the distribution of how far produced segments fell from the playlist (median and worst, not one extreme), how many keyframes were read from the container, and — the discriminator — **how many of the disagreeing segments began at ANOTHER time the same table names**. That separates the two explanations that have been argued rather than measured: a table describing times the file does not have, against a table listing only some keyframes with our grid built over its gaps. Every deviation measured on 2026-08-17 was positive, 0.58-2.96 s, which is what a cut pushed forward to the next real keyframe looks like. The summary is also written every 25 distinct boundaries instead of only when a session is disposed, because a proxy restart — every addon update is one — takes its sessions with it and the summary was routinely never written.
|
package/package.json
CHANGED
|
@@ -4896,6 +4896,44 @@ export class HlsSessionManager {
|
|
|
4896
4896
|
`transcode ${session.id} boundary #${index} corrected ${wasAt.toFixed(3)}s → ` +
|
|
4897
4897
|
`${trueStart.toFixed(3)}s from the file itself`
|
|
4898
4898
|
);
|
|
4899
|
+
// And every OTHER member whose run begins at this very boundary is moved
|
|
4900
|
+
// to the same instant.
|
|
4901
|
+
//
|
|
4902
|
+
// Why they were not there already: the two branches are asked for the same
|
|
4903
|
+
// time and land in different places. The picture cannot begin anywhere but
|
|
4904
|
+
// a real keyframe, and it may not begin before the time asked for — that
|
|
4905
|
+
// content belongs to the previous segment — so it moves FORWARD to the next
|
|
4906
|
+
// one, by up to the keyframe spacing (0.58-2.96 s measured 2026-08-17).
|
|
4907
|
+
// A soundtrack has no keyframes: it begins exactly where asked, to within
|
|
4908
|
+
// one audio frame. So after every restart the two runs of one film began up
|
|
4909
|
+
// to three seconds apart, each correctly labelled with where it really was,
|
|
4910
|
+
// and the viewer got sound with no new picture for the difference.
|
|
4911
|
+
//
|
|
4912
|
+
// The picture's true start is a MEASURED quantity — read from the piece it
|
|
4913
|
+
// just produced, which is what the correction above is — so the soundtrack
|
|
4914
|
+
// can be put exactly there instead of at the time the container's table
|
|
4915
|
+
// claimed. It converges: once the boundary holds the true time, the next
|
|
4916
|
+
// reading agrees with it and the guard above returns before doing anything.
|
|
4917
|
+
for (const member of this.#familyOf(session)) {
|
|
4918
|
+
if (member === session || member.encodeStartIndex !== index) {
|
|
4919
|
+
continue;
|
|
4920
|
+
}
|
|
4921
|
+
if (!processCanBeSignalled(member.runState)) {
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4924
|
+
logger.info(
|
|
4925
|
+
`transcode ${member.id} begins at #${index}, which really starts ` +
|
|
4926
|
+
`${(trueStart - wasAt).toFixed(3)}s later than the table said — restarting it there ` +
|
|
4927
|
+
`so picture and sound begin together`
|
|
4928
|
+
);
|
|
4929
|
+
// Restarted at the same INDEX, deliberately, rather than seeked to the
|
|
4930
|
+
// time: a seek decides by index, finds this run already begins at #index,
|
|
4931
|
+
// and answers "already within the running encode" — which is true about
|
|
4932
|
+
// the index and false about the instant, and it is why the first version
|
|
4933
|
+
// of this fix moved nothing at all. The boundary now holds the corrected
|
|
4934
|
+
// time, so starting the run at this index starts it at that time.
|
|
4935
|
+
void this.#startEncodeRun(member, index).catch(() => {});
|
|
4936
|
+
}
|
|
4899
4937
|
}
|
|
4900
4938
|
|
|
4901
4939
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Picture and sound must begin their runs at the same real instant.
|
|
3
|
+
*
|
|
4
|
+
* The two branches are asked for the same time and land in different places: a
|
|
5
|
+
* copied picture may only begin at a real keyframe and may not begin before the
|
|
6
|
+
* time asked for, so it moves FORWARD to the next one; a soundtrack has no
|
|
7
|
+
* keyframes and begins exactly where asked. Measured 2026-08-17, the difference
|
|
8
|
+
* was 0.58-2.96 s on one file, and the viewer got sound with no new picture for
|
|
9
|
+
* as long as it lasted.
|
|
10
|
+
*
|
|
11
|
+
* The picture's true start is measured from the piece it produced. This pins
|
|
12
|
+
* that the measurement is carried to the other members of the family.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
|
|
18
|
+
import { HlsSessionManager } from "../services/hls-session-manager.js";
|
|
19
|
+
import { ENCODE_RUN_STATE, INITIAL_RUN_STATE } from "../services/encode-run-state.js";
|
|
20
|
+
|
|
21
|
+
const BOUNDARIES = [0, 4, 8, 12, 16, 20];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A film's family: the picture, and a soundtrack rendition of it. Both runs
|
|
25
|
+
* begin at boundary #2, which the container's table puts at 8 s.
|
|
26
|
+
*
|
|
27
|
+
* @returns {{ manager: HlsSessionManager, picture: object, sound: object }}
|
|
28
|
+
*/
|
|
29
|
+
function familyAtBoundaryTwo() {
|
|
30
|
+
const manager = new HlsSessionManager({
|
|
31
|
+
enabled: true,
|
|
32
|
+
ffmpegBin: "ffmpeg",
|
|
33
|
+
localBindHost: "127.0.0.1",
|
|
34
|
+
localPort: 9090
|
|
35
|
+
});
|
|
36
|
+
const picture = {
|
|
37
|
+
id: "picture",
|
|
38
|
+
state: "ready",
|
|
39
|
+
runState: ENCODE_RUN_STATE.PRODUCING,
|
|
40
|
+
segmentBoundaries: [...BOUNDARIES],
|
|
41
|
+
encodeStartIndex: 2,
|
|
42
|
+
audioRenditionSessions: new Map([[1, "sound"]]),
|
|
43
|
+
indexCheck: null
|
|
44
|
+
};
|
|
45
|
+
const sound = {
|
|
46
|
+
id: "sound",
|
|
47
|
+
state: "ready",
|
|
48
|
+
runState: ENCODE_RUN_STATE.PRODUCING,
|
|
49
|
+
audioOnly: true,
|
|
50
|
+
baseSessionId: "picture",
|
|
51
|
+
segmentBoundaries: [...BOUNDARIES],
|
|
52
|
+
encodeStartIndex: 2,
|
|
53
|
+
runSerial: 0,
|
|
54
|
+
indexCheck: null
|
|
55
|
+
};
|
|
56
|
+
manager.sessionsById.set("picture", picture);
|
|
57
|
+
manager.sessionsById.set("sound", sound);
|
|
58
|
+
|
|
59
|
+
return { manager, picture, sound };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test("a soundtrack follows the picture to the instant the picture really began", () => {
|
|
63
|
+
const { manager, picture, sound } = familyAtBoundaryTwo();
|
|
64
|
+
const runsBefore = sound.runSerial;
|
|
65
|
+
|
|
66
|
+
manager.correctBoundaryFromSegment(picture, 2, 10.5);
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
picture.segmentBoundaries,
|
|
70
|
+
[0, 4, 10.5, 12, 16, 20],
|
|
71
|
+
"the family's table must hold what the file itself said"
|
|
72
|
+
);
|
|
73
|
+
assert.equal(
|
|
74
|
+
sound.segmentBoundaries[2],
|
|
75
|
+
10.5,
|
|
76
|
+
"and every member's table with it — one film, one timeline"
|
|
77
|
+
);
|
|
78
|
+
// A NEW run, not a seek. A seek decides by index, finds the soundtrack
|
|
79
|
+
// already begins at #2 and answers "already within the running encode" —
|
|
80
|
+
// true about the index, false about the instant. The first version of this
|
|
81
|
+
// fix did exactly that and moved nothing.
|
|
82
|
+
//
|
|
83
|
+
// `runSerial` is the evidence because it is the first thing a run start
|
|
84
|
+
// writes, before it awaits anything: the assertion then holds without the
|
|
85
|
+
// test needing a filesystem, a process, or a guess about how many ticks to
|
|
86
|
+
// wait for one.
|
|
87
|
+
assert.equal(
|
|
88
|
+
sound.runSerial,
|
|
89
|
+
runsBefore + 1,
|
|
90
|
+
"the soundtrack's run must be started again, at the corrected time"
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a correction the table already holds moves nobody", () => {
|
|
95
|
+
const { manager, picture, sound } = familyAtBoundaryTwo();
|
|
96
|
+
const before = [...sound.segmentBoundaries];
|
|
97
|
+
// Within the tolerance: the reading agrees with the table, so there is
|
|
98
|
+
// nothing to correct and nothing to move. This is what makes the repositioning
|
|
99
|
+
// converge instead of repeating on every produced segment.
|
|
100
|
+
manager.correctBoundaryFromSegment(picture, 2, 8.1);
|
|
101
|
+
assert.deepEqual(sound.segmentBoundaries, before);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a member that is not running is left alone", () => {
|
|
105
|
+
const { manager, picture, sound } = familyAtBoundaryTwo();
|
|
106
|
+
// A rung the viewer switched away from has no process. Moving it would start
|
|
107
|
+
// an encoder for nobody — the failure that put three ffmpeg runs on one file.
|
|
108
|
+
sound.runState = ENCODE_RUN_STATE.STOPPED;
|
|
109
|
+
manager.correctBoundaryFromSegment(picture, 2, 10.5);
|
|
110
|
+
assert.equal(sound.runSerial, 0, "a stopped member is not started again for nobody");
|
|
111
|
+
assert.equal(sound.encodeStartIndex, 2, "a stopped member keeps its place and its silence");
|
|
112
|
+
assert.equal(sound.runState, ENCODE_RUN_STATE.STOPPED);
|
|
113
|
+
assert.notEqual(INITIAL_RUN_STATE, ENCODE_RUN_STATE.STOPPED);
|
|
114
|
+
});
|