@torrent-tv/proxy 2.83.8 → 2.84.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.
@@ -42,7 +42,7 @@ async function managerAfterSeek() {
42
42
  localBindHost: "127.0.0.1",
43
43
  localPort: 9090
44
44
  });
45
- manager.sessionsById.set(SESSION_ID, {
45
+ const session = {
46
46
  id: SESSION_ID,
47
47
  dirPath,
48
48
  // Where this file is cut, held by the file. A fixture that stated it
@@ -66,9 +66,11 @@ async function managerAfterSeek() {
66
66
  segmentCount: 399,
67
67
  encodeStartIndex: SEGMENT_AT_SEEK,
68
68
  waitEpoch: 1,
69
- furthestViewerSeconds: SEEK_TO_SECONDS,
70
69
  runState: "PRODUCING"
71
- });
70
+ };
71
+ manager.sessionsById.set(SESSION_ID, session);
72
+ viewerOf(session, "").moveTo(SEEK_TO_SECONDS);
73
+ viewerOf(session, "").playing = false;
72
74
  return { manager, dirPath };
73
75
  }
74
76
 
@@ -141,15 +143,15 @@ test("the viewer who made the request is the one it is judged against", async ()
141
143
  const { manager, dirPath } = await managerAfterSeek();
142
144
  const session = manager.sessionsById.get(SESSION_ID);
143
145
  session.viewers = new Map();
144
- // Two people watching one copied picture. The shared position belongs to the
145
- // one in front — it is the furthest segment anybody asked for — and the one
146
- // behind is a hundred segments back, waiting for a segment there.
146
+ // Two people watching one copied picture: one at the seek target, one a
147
+ // hundred segments back and waiting for a segment there. Asked about nobody
148
+ // in particular the answer is the FURTHEST of them, because what lies behind
149
+ // that one has already been made.
147
150
  const behind = SEGMENT_AT_SEEK - 100;
148
- viewerOf(session, "behind").position = {
149
- segment: behind,
150
- seconds: behind * SEGMENT_SECONDS,
151
- at: Date.now()
152
- };
151
+ viewerOf(session, "behind").moveTo(behind * SEGMENT_SECONDS);
152
+ viewerOf(session, "behind").playing = false;
153
+ viewerOf(session, "ahead").moveTo(SEEK_TO_SECONDS);
154
+ viewerOf(session, "ahead").playing = false;
153
155
 
154
156
  assert.equal(
155
157
  manager.requestStillWanted(SESSION_ID, `segment-${String(behind).padStart(5, "0")}.mp4`, "behind"),
@@ -159,7 +161,7 @@ test("the viewer who made the request is the one it is judged against", async ()
159
161
  assert.equal(
160
162
  manager.requestStillWanted(SESSION_ID, `segment-${String(behind).padStart(5, "0")}.mp4`),
161
163
  false,
162
- "unnamed, it can only be judged against the shared position — which is the leader's"
164
+ "unnamed, it is judged against the furthest viewer, which is the leader"
163
165
  );
164
166
 
165
167
  await rm(dirPath, { recursive: true, force: true });
@@ -155,9 +155,8 @@ test("a request cannot move the viewer's position backwards", async (t) => {
155
155
  manager.requestSeek(SESSION_ID, 2083.4, "viewer-1");
156
156
  await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(BEHIND_INDEX), { requestSeq: 1 });
157
157
 
158
- assert.equal(
159
- manager.viewers.get("viewer-1")?.positionSeconds(),
160
- 2083.4,
158
+ assert.ok(
159
+ Math.abs((manager.viewers.get("viewer-1")?.positionSeconds() ?? -1) - 2083.4) < 1,
161
160
  "a stale request must not rewrite what the viewer said about themselves"
162
161
  );
163
162
  });
@@ -1,108 +1,115 @@
1
- /**
2
- * @file One object per person watching, and one deletion to forget them.
3
- *
4
- * It was six maps hung on the session, each keyed by consumer id, and the
5
- * forgetting was already wrong: releasing a consumer emptied none of them, so a
6
- * viewer who had left went on counting as wanting their soundtrack until their
7
- * head expired, and their entries stayed for the life of the session.
8
- */
9
-
10
- import test from "node:test";
11
- import assert from "node:assert/strict";
12
- import { SourceFile } from "../services/source/SourceFile.js";
13
- import { mkdtemp, rm } from "node:fs/promises";
14
- import os from "node:os";
15
- import path from "node:path";
16
- import { HlsSessionManager } from "../services/hls-session-manager.js";
17
- import { Viewer, viewerOf, viewersOf } from "../services/viewer/Viewer.js";
18
-
19
- const SESSION_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
20
-
21
- test("presence and position are two facts, and presence does not wait for a request", () => {
22
- const now = 1_000_000;
23
- const viewer = new Viewer("someone", now);
24
-
25
- // The whole of the 2026-09-05 failure in one assertion: a viewer who has
26
- // arrived and asked for nothing yet IS watching. Answered from the position
27
- // alone — which is written only when a segment is requested — this read
28
- // false, every encoder on their output was stopped for having nobody, and
29
- // the `init.mp4` they needed in order to request their first segment was
30
- // therefore never made.
31
- assert.equal(viewer.position, null, "nothing has placed them yet");
32
- assert.equal(viewer.isPresent(now, 60_000), true, "and they are still here");
33
-
34
- viewer.seen(now - 10_000);
35
- assert.equal(viewer.isPresent(now, 60_000), true);
36
-
37
- // Silence longer than any a watching viewer can produce. This is a backstop
38
- // for a connection that failed to say so, not the ordinary way of leaving.
39
- viewer.seen(now - 120_000);
40
- assert.equal(viewer.isPresent(now, 60_000), false);
41
-
42
- // Something SAID they are gone. Then it does not matter how recently they
43
- // were heard from.
44
- viewer.seen(now);
45
- viewer.gone = true;
46
- assert.equal(viewer.isPresent(now, 60_000), false, "a statement outranks silence");
47
- });
48
-
49
- test("a stated position outranks the segment they last asked for", () => {
50
- const viewer = new Viewer("someone");
51
- assert.equal(viewer.positionSeconds(), null);
52
-
53
- viewer.position = { segment: 10, seconds: 40, at: 1 };
54
- assert.equal(viewer.positionSeconds(), 40, "where their player is reading");
55
-
56
- viewer.position = { segment: 10, seconds: 40, at: 1, seeked: 900 };
57
- assert.equal(viewer.positionSeconds(), 900, "a seek is the viewer saying where they are");
58
- });
59
-
60
- test("asking for a viewer twice is the same viewer", () => {
61
- const session = {};
62
- const first = viewerOf(session, "one");
63
- first.audio = { trackIndex: 3, transcode: false };
64
-
65
- assert.equal(viewerOf(session, "one"), first);
66
- assert.equal(viewerOf(session, "one").audio.trackIndex, 3);
67
- assert.notEqual(viewerOf(session, "two"), first);
68
- assert.equal(viewersOf(session).size, 2);
69
- });
70
-
71
- test("releasing a consumer forgets everything that was true of them alone", async (t) => {
72
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "viewer-"));
73
- t.after(async () => {
74
- await rm(dirPath, { recursive: true, force: true });
75
- });
76
- const manager = new HlsSessionManager({
77
- enabled: true,
78
- ffmpegBin: "ffmpeg",
79
- localBindHost: "127.0.0.1",
80
- localPort: 9090
81
- });
82
- const session = {
83
- id: SESSION_ID,
84
- dirPath,
85
- state: "live",
86
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
87
- // An ordinary session reads its own file, and its sound is inside it. The
88
- // three differ only for a soundtrack shipped as a file of its own.
89
- get inputFile() { return this.file; },
90
- get audioFile() { return this.file; },
91
- consumers: new Set(["staying", "leaving"]),
92
- viewers: new Map(),
93
- lastAccessedAt: Date.now()
94
- };
95
- manager.sessionsById.set(SESSION_ID, session);
96
-
97
- const leaving = viewerOf(session, "leaving");
98
- leaving.audio = { trackIndex: 2, transcode: true };
99
- leaving.activeVariantId = "some-variant";
100
- leaving.position = { segment: 40, seconds: 160, at: Date.now() };
101
- viewerOf(session, "staying").audio = { trackIndex: 0, transcode: false };
102
-
103
- await manager.releaseSessionConsumer(SESSION_ID, "leaving", "the tab was closed");
104
-
105
- assert.equal(session.viewers.has("leaving"), false, "one deletion, not six");
106
- assert.equal(session.viewers.has("staying"), true, "and it takes nobody else with it");
107
- assert.equal(session.consumers.size, 1);
108
- });
1
+ /**
2
+ * @file One object per person watching, and one deletion to forget them.
3
+ *
4
+ * It was six maps hung on the session, each keyed by consumer id, and the
5
+ * forgetting was already wrong: releasing a consumer emptied none of them, so a
6
+ * viewer who had left went on counting as wanting their soundtrack until their
7
+ * head expired, and their entries stayed for the life of the session.
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { SourceFile } from "../services/source/SourceFile.js";
13
+ import { mkdtemp, rm } from "node:fs/promises";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { HlsSessionManager } from "../services/hls-session-manager.js";
17
+ import { Viewer, viewerOf, viewersOf } from "../services/viewer/Viewer.js";
18
+
19
+ const SESSION_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
20
+
21
+ test("presence and position are two facts, and presence does not wait for a request", () => {
22
+ const now = 1_000_000;
23
+ const viewer = new Viewer("someone", now);
24
+
25
+ // The whole of the 2026-09-05 failure in one assertion: a viewer who has
26
+ // arrived and asked for nothing yet IS watching. Answered from the position
27
+ // alone — which is written only when a segment is requested — this read
28
+ // false, every encoder on their output was stopped for having nobody, and
29
+ // the `init.mp4` they needed in order to request their first segment was
30
+ // therefore never made.
31
+ assert.equal(viewer.position, null, "nothing has placed them yet");
32
+ assert.equal(viewer.isPresent(), true, "and they are still here");
33
+
34
+ // SILENCE IS NOT ABSENCE, however long it lasts. A paused viewer, a hidden
35
+ // tab whose timers the browser has throttled, and one holding two minutes of
36
+ // cushion all say nothing and are all still watching — so there is no
37
+ // interval that separates them from someone who has left, and the threshold
38
+ // that used to try is gone.
39
+ viewer.seen(now - 600_000);
40
+ assert.equal(viewer.isPresent(), true, "ten minutes of silence is not leaving");
41
+
42
+ // Something SAID they are gone: the browser released the session, or their
43
+ // connection closed. That is the only way out.
44
+ viewer.gone = true;
45
+ assert.equal(viewer.isPresent(), false, "a statement is what ends it");
46
+ });
47
+
48
+ test("position is one fact with one writer, and a function of time", () => {
49
+ const viewer = new Viewer("someone");
50
+ assert.equal(viewer.positionSeconds(), null);
51
+
52
+ viewer.moveTo(40, 1_000_000);
53
+ viewer.playing = false;
54
+ assert.equal(viewer.positionSeconds(1_000_000), 40);
55
+ assert.equal(viewer.positionSeconds(1_060_000), 40, "a stopped picture covers no film");
56
+
57
+ viewer.playing = true;
58
+ assert.equal(viewer.positionSeconds(1_010_000), 50, "a playing one covers a second a second");
59
+
60
+ // A seek is the same statement, and there is no second field for it to win
61
+ // against: two writers taking turns on one reading is what made the priority
62
+ // map jump backwards several times a second in the field on 2026-09-13.
63
+ viewer.moveTo(900, 1_010_000);
64
+ assert.equal(viewer.positionSeconds(1_010_000), 900);
65
+ });
66
+
67
+ test("asking for a viewer twice is the same viewer", () => {
68
+ const session = {};
69
+ const first = viewerOf(session, "one");
70
+ first.audio = { trackIndex: 3, transcode: false };
71
+
72
+ assert.equal(viewerOf(session, "one"), first);
73
+ assert.equal(viewerOf(session, "one").audio.trackIndex, 3);
74
+ assert.notEqual(viewerOf(session, "two"), first);
75
+ assert.equal(viewersOf(session).size, 2);
76
+ });
77
+
78
+ test("releasing a consumer forgets everything that was true of them alone", async (t) => {
79
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "viewer-"));
80
+ t.after(async () => {
81
+ await rm(dirPath, { recursive: true, force: true });
82
+ });
83
+ const manager = new HlsSessionManager({
84
+ enabled: true,
85
+ ffmpegBin: "ffmpeg",
86
+ localBindHost: "127.0.0.1",
87
+ localPort: 9090
88
+ });
89
+ const session = {
90
+ id: SESSION_ID,
91
+ dirPath,
92
+ state: "live",
93
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
94
+ // An ordinary session reads its own file, and its sound is inside it. The
95
+ // three differ only for a soundtrack shipped as a file of its own.
96
+ get inputFile() { return this.file; },
97
+ get audioFile() { return this.file; },
98
+ consumers: new Set(["staying", "leaving"]),
99
+ viewers: new Map(),
100
+ lastAccessedAt: Date.now()
101
+ };
102
+ manager.sessionsById.set(SESSION_ID, session);
103
+
104
+ const leaving = viewerOf(session, "leaving");
105
+ leaving.audio = { trackIndex: 2, transcode: true };
106
+ leaving.activeVariantId = "some-variant";
107
+ leaving.position = { segment: 40, seconds: 160, at: Date.now() };
108
+ viewerOf(session, "staying").audio = { trackIndex: 0, transcode: false };
109
+
110
+ await manager.releaseSessionConsumer(SESSION_ID, "leaving", "the tab was closed");
111
+
112
+ assert.equal(session.viewers.has("leaving"), false, "one deletion, not six");
113
+ assert.equal(session.viewers.has("staying"), true, "and it takes nobody else with it");
114
+ assert.equal(session.consumers.size, 1);
115
+ });
@@ -1,92 +0,0 @@
1
- /**
2
- * @file Opening a file at a position puts the SOUND there too.
3
- *
4
- * The audio rendition is a session of its own, and where it starts is computed
5
- * from where the viewer is. That reading had three sources and only two were
6
- * consulted — a position seeked to, and the last segment this session served —
7
- * both of which are written by things that have not happened yet at the moment
8
- * a file is opened at a position. So the answer was zero.
9
- *
10
- * Field 2026-08-21, `Minions.and.Monsters.1080p.mkv` reopened from the address
11
- * bar at 52:07:
12
- *
13
- * 18:02:55.124 8ed85605 start=3130s -ss 3125.25 -an -map 0:v:0 -c:v copy
14
- * 18:02:55.632 33b0b046 start=0s no -ss -vn -map 0:a:0 -c:a aac
15
- *
16
- * The picture went to segment #781, the sound to #0. The player asked both for
17
- * #782; the picture had it, the sound re-encoded 57.5 s of a 3130 s film in the
18
- * 45 s the request lasted and then answered 404 — shown to the viewer as "the
19
- * proxy accepted the request but sent no video".
20
- */
21
-
22
- import assert from "node:assert/strict";
23
- import test from "node:test";
24
-
25
- import { resolveViewerPosition, viewerPositionSource } from "../services/hls-session-manager.js";
26
-
27
- test("a file opened at a position has its viewer at that position", () => {
28
- // Nothing has been seeked and nothing served yet — the state at the instant
29
- // the audio rendition is created.
30
- assert.equal(resolveViewerPosition({ openedAt: 3130 }), 3130);
31
- });
32
-
33
- test("a seek beats everything else", () => {
34
- assert.equal(
35
- resolveViewerPosition({ seeked: 900, lastRequestedStart: 400, openedAt: 3130 }),
36
- 900
37
- );
38
- });
39
-
40
- test("what has been served beats where the file was opened", () => {
41
- // The opening position is the oldest of the three readings: once a segment
42
- // has been served, that is where the reading is.
43
- assert.equal(resolveViewerPosition({ lastRequestedStart: 400, openedAt: 3130 }), 400);
44
- });
45
-
46
- test("with nothing to go on the answer is the beginning", () => {
47
- assert.equal(resolveViewerPosition({}), 0);
48
- assert.equal(resolveViewerPosition({ seeked: 0, lastRequestedStart: 0, openedAt: 0 }), 0);
49
- // Values that are not readings must not become one.
50
- assert.equal(resolveViewerPosition({ seeked: Number.NaN, openedAt: Number.NaN }), 0);
51
- assert.equal(resolveViewerPosition({ seeked: -5, openedAt: -5 }), 0);
52
- assert.equal(resolveViewerPosition({ lastRequestedStart: null, openedAt: undefined }), 0);
53
- });
54
-
55
- /**
56
- * Which of the three answered is a separate question, and the audio start needs
57
- * it. A seek and a served segment are request edges — the picture is behind
58
- * them by however deep the viewer's buffer is, which is what the subtraction in
59
- * `#audioStartSecondsFor` converts. The opening position is not an edge: it is
60
- * where the session was made, nothing has been asked for since, and a browser
61
- * that has just opened holds nothing.
62
- *
63
- * Field 2026-08-31: a page opened at 588s, no report yet, and the whole 120 s
64
- * look-ahead was subtracted — the sound started at 460s and its first segment
65
- * took 38.8 s to appear against the picture's 8.4 s.
66
- */
67
- test("the reading says which of the three it came from", () => {
68
- assert.equal(viewerPositionSource({ seeked: 900, lastRequestedStart: 400, openedAt: 3130 }), "seeked");
69
- assert.equal(viewerPositionSource({ lastRequestedStart: 400, openedAt: 3130 }), "requested");
70
- assert.equal(viewerPositionSource({ openedAt: 3130 }), "opened");
71
- assert.equal(viewerPositionSource({}), "none");
72
- });
73
-
74
- test("the source agrees with the position, reading for reading", () => {
75
- const readings = [
76
- { seeked: 900, lastRequestedStart: 400, openedAt: 3130 },
77
- { lastRequestedStart: 400, openedAt: 3130 },
78
- { openedAt: 3130 },
79
- { seeked: Number.NaN, openedAt: Number.NaN },
80
- { seeked: -5, openedAt: -5 },
81
- {}
82
- ];
83
- for (const reading of readings) {
84
- const position = resolveViewerPosition(reading);
85
- const source = viewerPositionSource(reading);
86
- assert.equal(
87
- source === "none",
88
- position === 0,
89
- `no source must mean no position, and the other way round: ${JSON.stringify(reading)}`
90
- );
91
- }
92
- });