@torrent-tv/proxy 2.76.6 → 2.78.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.
@@ -1,17 +1,17 @@
1
- /**
2
- * @file What a torrent's own names say about a track shipped as its own file.
3
- *
4
- * The rule worth pinning is the releaser one, because getting it wrong invents
5
- * an author. In the field case — `[HorribleSubs] Drifters - 02 [1080p].mka` in
6
- * `Rus Sound/`, beside `[HorribleSubs] Drifters - 02 [1080p].mkv` — the dub
7
- * carries the picture's own brackets and nothing else, so there is nothing
8
- * truthful to say about who made it. The subtitles of the same release sit in
9
- * `Sub/[Stan WarHammer & Nesitach]/`, a bracket the picture does not carry, and
10
- * that one IS their author.
11
- */
12
-
13
- import test from "node:test";
14
- import assert from "node:assert/strict";
1
+ /**
2
+ * @file What a torrent's own names say about a track shipped as its own file.
3
+ *
4
+ * The rule worth pinning is the releaser one, because getting it wrong invents
5
+ * an author. In the field case — `[HorribleSubs] Drifters - 02 [1080p].mka` in
6
+ * `Rus Sound/`, beside `[HorribleSubs] Drifters - 02 [1080p].mkv` — the dub
7
+ * carries the picture's own brackets and nothing else, so there is nothing
8
+ * truthful to say about who made it. The subtitles of the same release sit in
9
+ * `Sub/[Stan WarHammer & Nesitach]/`, a bracket the picture does not carry, and
10
+ * that one IS their author.
11
+ */
12
+
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
15
  import {
16
16
  languageFromFolderNames,
17
17
  releaserFrom,
@@ -34,109 +34,109 @@ function readPath(relativePath, videoName = "") {
34
34
  });
35
35
  return { ...naming, code: naming.code ?? "und", group: naming.releaser };
36
36
  }
37
-
38
-
39
- const VIDEO_NAME = "[HorribleSubs] Drifters - 02 [1080p].mkv";
40
-
41
- test("a folder naming a language is read, whole segment or word", () => {
42
- assert.equal(languageFromFolderNames(["ENG"])?.code, "en");
43
- assert.equal(languageFromFolderNames(["Rus Sound"])?.code, "ru");
44
- assert.equal(languageFromFolderNames(["Sub", "Russian"])?.code, "ru");
45
- assert.equal(languageFromFolderNames([])?.code, undefined);
46
- });
47
-
48
- test("the innermost folder answers first", () => {
49
- assert.equal(languageFromFolderNames(["Russian", "English Dub"])?.code, "en");
50
- });
51
-
52
- test("a two-letter word inside a longer folder name is not taken for a language", () => {
53
- // "No" is Norwegian as a whole segment, but a word in `No Subs` is a word.
54
- assert.equal(languageFromFolderNames(["No Subs"]), null);
55
- assert.equal(languageFromFolderNames(["no"])?.code, "no");
56
- });
57
-
58
- test("a bracket the picture also carries is not the sidecar's author", () => {
59
- assert.equal(
60
- releaserFrom({
61
- folders: ["Rus Sound"],
62
- fileName: "[HorribleSubs] Drifters - 02 [1080p].mka",
63
- videoName: VIDEO_NAME
64
- }),
65
- null
66
- );
67
- });
68
-
69
- test("a bracket only the sidecar carries is its author", () => {
70
- assert.equal(
71
- releaserFrom({
72
- folders: ["Sub", "[Stan WarHammer & Nesitach]"],
73
- fileName: "[HorribleSubs] Drifters - 02 [1080p].ass",
74
- videoName: VIDEO_NAME
75
- }),
76
- "Stan WarHammer & Nesitach"
77
- );
78
- });
79
-
80
- test("a bracket describing the encode is never an author", () => {
81
- for (const token of ["1080p", "x264", "HEVC", "WEB-DL", "AAC", "5.1", "10bit", "78EFD746", "RUS"]) {
82
- assert.equal(
83
- releaserFrom({ folders: [], fileName: `Film [${token}].mka`, videoName: "Other.mkv" }),
84
- null,
85
- `[${token}] should not be read as a releaser`
86
- );
87
- }
88
- });
89
-
90
- test("the innermost folder is preferred to the file name", () => {
91
- assert.equal(
92
- releaserFrom({ folders: ["[TeamA]"], fileName: "[TeamB] ep.ass", videoName: "ep.mkv" }),
93
- "TeamA"
94
- );
95
- });
96
-
97
- test("language and author together, for the field case", () => {
98
- assert.deepEqual(
99
- sidecarNaming({
100
- folders: ["Rus Sound"],
101
- fileName: "[HorribleSubs] Drifters - 02 [1080p].mka",
102
- videoName: VIDEO_NAME
103
- }),
104
- { code: "ru", name: "Russian", releaser: null, isForced: false, isHearingImpaired: false, isDefault: false }
105
- );
106
- assert.deepEqual(
107
- sidecarNaming({
108
- folders: ["Sub", "[Stan WarHammer & Nesitach]"],
109
- fileName: "[HorribleSubs] Drifters - 02 [1080p].ass",
110
- videoName: VIDEO_NAME
111
- }),
112
- {
113
- code: null,
114
- name: null,
115
- releaser: "Stan WarHammer & Nesitach",
116
- isForced: false,
117
- isHearingImpaired: false,
118
- isDefault: false
119
- }
120
- );
121
- });
122
-
123
- test("a subtitle file takes its author from the folder the release put it in", () => {
124
- const info = readPath("Sub/[Stan WarHammer & Nesitach]/[HorribleSubs] Drifters - 02 [1080p].ass", VIDEO_NAME);
125
- assert.equal(info.group, "Stan WarHammer & Nesitach");
126
- });
127
-
128
- test("the picture's own release group is never reported as the translator", () => {
129
- const info = readPath("Sub/[HorribleSubs] Drifters - 02 [1080p].ass", VIDEO_NAME);
130
- assert.equal(info.group, null);
131
- });
132
-
133
- test("the language of a subtitle folder is read where the whole segment is not a language", () => {
134
- const info = readPath("Rus Sub/ep.srt", "ep.mkv");
135
- assert.equal(info.code, "ru");
136
- });
137
-
138
- test("what the rules answer with nothing to go on", () => {
139
- const info = readPath("ep.srt");
140
- assert.equal(info.code, "und");
141
- assert.equal(info.group, null);
142
- });
37
+
38
+
39
+ const VIDEO_NAME = "[HorribleSubs] Drifters - 02 [1080p].mkv";
40
+
41
+ test("a folder naming a language is read, whole segment or word", () => {
42
+ assert.equal(languageFromFolderNames(["ENG"])?.code, "en");
43
+ assert.equal(languageFromFolderNames(["Rus Sound"])?.code, "ru");
44
+ assert.equal(languageFromFolderNames(["Sub", "Russian"])?.code, "ru");
45
+ assert.equal(languageFromFolderNames([])?.code, undefined);
46
+ });
47
+
48
+ test("the innermost folder answers first", () => {
49
+ assert.equal(languageFromFolderNames(["Russian", "English Dub"])?.code, "en");
50
+ });
51
+
52
+ test("a two-letter word inside a longer folder name is not taken for a language", () => {
53
+ // "No" is Norwegian as a whole segment, but a word in `No Subs` is a word.
54
+ assert.equal(languageFromFolderNames(["No Subs"]), null);
55
+ assert.equal(languageFromFolderNames(["no"])?.code, "no");
56
+ });
57
+
58
+ test("a bracket the picture also carries is not the sidecar's author", () => {
59
+ assert.equal(
60
+ releaserFrom({
61
+ folders: ["Rus Sound"],
62
+ fileName: "[HorribleSubs] Drifters - 02 [1080p].mka",
63
+ videoName: VIDEO_NAME
64
+ }),
65
+ null
66
+ );
67
+ });
68
+
69
+ test("a bracket only the sidecar carries is its author", () => {
70
+ assert.equal(
71
+ releaserFrom({
72
+ folders: ["Sub", "[Stan WarHammer & Nesitach]"],
73
+ fileName: "[HorribleSubs] Drifters - 02 [1080p].ass",
74
+ videoName: VIDEO_NAME
75
+ }),
76
+ "Stan WarHammer & Nesitach"
77
+ );
78
+ });
79
+
80
+ test("a bracket describing the encode is never an author", () => {
81
+ for (const token of ["1080p", "x264", "HEVC", "WEB-DL", "AAC", "5.1", "10bit", "78EFD746", "RUS"]) {
82
+ assert.equal(
83
+ releaserFrom({ folders: [], fileName: `Film [${token}].mka`, videoName: "Other.mkv" }),
84
+ null,
85
+ `[${token}] should not be read as a releaser`
86
+ );
87
+ }
88
+ });
89
+
90
+ test("the innermost folder is preferred to the file name", () => {
91
+ assert.equal(
92
+ releaserFrom({ folders: ["[TeamA]"], fileName: "[TeamB] ep.ass", videoName: "ep.mkv" }),
93
+ "TeamA"
94
+ );
95
+ });
96
+
97
+ test("language and author together, for the field case", () => {
98
+ assert.deepEqual(
99
+ sidecarNaming({
100
+ folders: ["Rus Sound"],
101
+ fileName: "[HorribleSubs] Drifters - 02 [1080p].mka",
102
+ videoName: VIDEO_NAME
103
+ }),
104
+ { code: "ru", name: "Russian", releaser: null, isForced: false, isHearingImpaired: false, isDefault: false }
105
+ );
106
+ assert.deepEqual(
107
+ sidecarNaming({
108
+ folders: ["Sub", "[Stan WarHammer & Nesitach]"],
109
+ fileName: "[HorribleSubs] Drifters - 02 [1080p].ass",
110
+ videoName: VIDEO_NAME
111
+ }),
112
+ {
113
+ code: null,
114
+ name: null,
115
+ releaser: "Stan WarHammer & Nesitach",
116
+ isForced: false,
117
+ isHearingImpaired: false,
118
+ isDefault: false
119
+ }
120
+ );
121
+ });
122
+
123
+ test("a subtitle file takes its author from the folder the release put it in", () => {
124
+ const info = readPath("Sub/[Stan WarHammer & Nesitach]/[HorribleSubs] Drifters - 02 [1080p].ass", VIDEO_NAME);
125
+ assert.equal(info.group, "Stan WarHammer & Nesitach");
126
+ });
127
+
128
+ test("the picture's own release group is never reported as the translator", () => {
129
+ const info = readPath("Sub/[HorribleSubs] Drifters - 02 [1080p].ass", VIDEO_NAME);
130
+ assert.equal(info.group, null);
131
+ });
132
+
133
+ test("the language of a subtitle folder is read where the whole segment is not a language", () => {
134
+ const info = readPath("Rus Sub/ep.srt", "ep.mkv");
135
+ assert.equal(info.code, "ru");
136
+ });
137
+
138
+ test("what the rules answer with nothing to go on", () => {
139
+ const info = readPath("ep.srt");
140
+ assert.equal(info.code, "und");
141
+ assert.equal(info.group, null);
142
+ });
@@ -79,8 +79,8 @@ async function sessionWithRunAt373() {
79
79
  holdExplainedAt: new Map(),
80
80
  seekSettleTimer: null,
81
81
  seekTarget: null,
82
- seekFailureTarget: -1,
83
- seekFailureCount: 0,
82
+ failedStartAt: -1,
83
+ failedStartCount: 0,
84
84
  waitEpoch: 0,
85
85
  firstSegmentLogged: true,
86
86
  progress: { processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS, speed: "1.0x", startPositionSeconds: RUN_STARTS_AT * SEGMENT_SECONDS }
@@ -114,8 +114,8 @@ async function pictureWithTwoViewers() {
114
114
  base.consumers = new Set([FIRST, SECOND]);
115
115
  // Both viewers are watching the picture, which is what keeps their choices
116
116
  // alive; a viewer whose head has expired holds no encoder.
117
- viewerOf(base, FIRST).head = { segment: 3, seconds: 12, at: Date.now() };
118
- viewerOf(base, SECOND).head = { segment: 3, seconds: 12, at: Date.now() };
117
+ viewerOf(base, FIRST).position = { segment: 3, seconds: 12, at: Date.now() };
118
+ viewerOf(base, SECOND).position = { segment: 3, seconds: 12, at: Date.now() };
119
119
  viewerOf(base, FIRST).audio = { trackIndex: 0, transcode: true };
120
120
  viewerOf(base, SECOND).audio = { trackIndex: 1, transcode: true };
121
121
  manager.sessionsById.set(BASE_ID, base);
@@ -83,8 +83,8 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
83
83
  }),
84
84
  encodeRunGeneration: 0,
85
85
  lastRestartAt: 0,
86
- seekFailureTarget: -1,
87
- seekFailureCount: 0,
86
+ failedStartAt: -1,
87
+ failedStartCount: 0,
88
88
  seekSettleTimer: null,
89
89
  seekTarget: null,
90
90
  waitEpoch: 0,
@@ -144,7 +144,7 @@ test("a viewer who steps down, back to the picture's own height and down again k
144
144
  startRunOn(base, { process: fakeEncoder() });
145
145
  const viewer = manager.viewers.of(base, VIEWER);
146
146
  viewer.audio = { trackIndex: 1, transcode: true };
147
- viewer.head = { segment: 25, seconds: 100, at: Date.now() };
147
+ viewer.position = { segment: 25, seconds: 100, at: Date.now() };
148
148
 
149
149
  await manager.resolveVariantFile(BASE_ID, 540, "segment-00025.mp4", VIEWER);
150
150
  await manager.resolveVariantFile(BASE_ID, 812, "segment-00026.mp4", VIEWER);
@@ -165,7 +165,7 @@ test("a viewer leaving is subtracted from every output, and one nobody is left w
165
165
  t.after(() => rm(dirPath, { recursive: true, force: true }));
166
166
  // Watching all three, which is what a viewer who picked a language and a
167
167
  // quality is doing.
168
- manager.viewers.of(base, VIEWER).head = { segment: 25, seconds: 100, at: Date.now() };
168
+ manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
169
169
  manager.viewers.of(step, VIEWER);
170
170
  manager.viewers.of(audio, VIEWER);
171
171
  assert.deepEqual(
@@ -195,12 +195,12 @@ test("an output somebody else is still watching is kept when one viewer leaves",
195
195
  t.after(() => rm(dirPath, { recursive: true, force: true }));
196
196
  const second = "viewer-two";
197
197
  base.consumers.add(second);
198
- manager.viewers.of(base, VIEWER).head = { segment: 25, seconds: 100, at: Date.now() };
198
+ manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
199
199
  manager.viewers.of(step, VIEWER);
200
200
  manager.viewers.of(audio, VIEWER);
201
201
  // The second viewer is listening to the same soundtrack and watching the
202
202
  // picture at its own height.
203
- manager.viewers.of(base, second).head = { segment: 25, seconds: 100, at: Date.now() };
203
+ manager.viewers.of(base, second).position = { segment: 25, seconds: 100, at: Date.now() };
204
204
  manager.viewers.of(audio, second);
205
205
 
206
206
  await manager.releaseSessionConsumer(BASE_ID, VIEWER, "the first viewer left");
@@ -227,7 +227,7 @@ test("an output somebody else is still watching is kept when one viewer leaves",
227
227
  test("leaving an output releases what the watching claimed of production", async (t) => {
228
228
  const { manager, base, step, audio, dirPath, released } = await pictureWithStepAndSoundtrack();
229
229
  t.after(() => rm(dirPath, { recursive: true, force: true }));
230
- manager.viewers.of(base, VIEWER).head = { segment: 25, seconds: 100, at: Date.now() };
230
+ manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
231
231
  manager.viewers.of(step, VIEWER);
232
232
  manager.viewers.of(audio, VIEWER);
233
233
 
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @file Presence is a fact of the connection, and it is not the same fact as
3
+ * position.
4
+ *
5
+ * Both halves failed together on 2026-09-05. A viewer who had arrived and asked
6
+ * for nothing counted as absent, so a soundtrack's encoder was stopped 1.25 s
7
+ * after starting with nothing produced — and its `init.mp4` was therefore never
8
+ * made, which is the one file the viewer needed in order to ask for the segment
9
+ * that would have marked them present. And nothing anywhere let a viewer go
10
+ * when their connection closed: the only exits were the browser's own release,
11
+ * which a killed tab never sends, and a silence that a paused viewer produces
12
+ * just as well as a departed one.
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { mkdtemp, rm } from "node:fs/promises";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+ import { HlsSessionManager } from "../services/hls-session-manager.js";
21
+ import { SourceFile } from "../services/source/SourceFile.js";
22
+ import { Viewers } from "../services/viewer/Viewers.js";
23
+
24
+ const PICTURE = "aaaaaaaa-0000-4000-8000-000000000001";
25
+ const SOUND = "aaaaaaaa-0000-4000-8000-000000000002";
26
+
27
+ /**
28
+ * @param {string} id
29
+ * @param {string} dirPath
30
+ * @returns {object}
31
+ */
32
+ function outputOn(id, dirPath) {
33
+ return {
34
+ id,
35
+ dirPath,
36
+ state: "live",
37
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
38
+ get inputFile() { return this.file; },
39
+ get audioFile() { return this.file; },
40
+ consumers: new Set(),
41
+ viewers: new Map(),
42
+ lastAccessedAt: Date.now()
43
+ };
44
+ }
45
+
46
+ test("asking for a viewer of an output is that viewer watching it", () => {
47
+ let changes = 0;
48
+ const viewers = new Viewers({ onChange: () => { changes += 1; } });
49
+ const output = { id: "out-1", viewers: new Map() };
50
+
51
+ const first = viewers.of(output, "someone");
52
+ assert.equal(changes, 1, "a new relation is a change");
53
+ assert.equal(first.outputs.has("out-1"), true);
54
+
55
+ viewers.of(output, "someone");
56
+ assert.equal(changes, 1, "asking again about the same output changes nothing");
57
+
58
+ const second = { id: "out-2", viewers: new Map() };
59
+ viewers.of(second, "someone");
60
+ assert.equal(changes, 2, "a second output is a change");
61
+ assert.equal(viewers.get("someone")?.outputs.size, 2);
62
+ });
63
+
64
+ test("a connection closing takes the person off every output at once", () => {
65
+ const viewers = new Viewers();
66
+ const picture = { id: "picture", viewers: new Map() };
67
+ const sound = { id: "sound", viewers: new Map() };
68
+ const byId = new Map([["picture", picture], ["sound", sound]]);
69
+
70
+ viewers.of(picture, "watcher");
71
+ viewers.of(sound, "watcher");
72
+ viewers.of(picture, "other");
73
+
74
+ const left = viewers.hasGone("watcher", (id) => byId.get(id) ?? null);
75
+
76
+ assert.deepEqual(left.sort(), ["picture", "sound"], "both, not the one the browser holds an id for");
77
+ assert.equal(picture.viewers.has("watcher"), false);
78
+ assert.equal(sound.viewers.has("watcher"), false);
79
+ assert.equal(picture.viewers.has("other"), true, "and nobody else goes with them");
80
+ assert.equal(viewers.get("watcher"), null, "the registry does not keep what it has let go");
81
+ });
82
+
83
+ test("a viewer whose connection closed is let go of every output they were watching", async (t) => {
84
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "presence-"));
85
+ t.after(async () => {
86
+ await rm(dirPath, { recursive: true, force: true });
87
+ });
88
+ const manager = new HlsSessionManager({
89
+ enabled: true,
90
+ ffmpegBin: "ffmpeg",
91
+ localBindHost: "127.0.0.1",
92
+ localPort: 9090
93
+ });
94
+
95
+ const picture = outputOn(PICTURE, dirPath);
96
+ const sound = outputOn(SOUND, dirPath);
97
+ picture.consumers.add("watcher");
98
+ sound.consumers.add("watcher");
99
+ manager.sessionsById.set(PICTURE, picture);
100
+ manager.sessionsById.set(SOUND, sound);
101
+ manager.viewers.of(picture, "watcher");
102
+ manager.viewers.of(sound, "watcher");
103
+
104
+ const released = await manager.viewerHasGone("watcher", "the connection closed");
105
+
106
+ assert.equal(released, 2, "the picture and the soundtrack, from one statement");
107
+ assert.equal(manager.viewers.get("watcher"), null);
108
+ });
109
+
110
+ test("a viewer nobody knows costs nothing to let go of", async () => {
111
+ const manager = new HlsSessionManager({
112
+ enabled: true,
113
+ ffmpegBin: "ffmpeg",
114
+ localBindHost: "127.0.0.1",
115
+ localPort: 9090
116
+ });
117
+ assert.equal(await manager.viewerHasGone("never-seen"), 0);
118
+ assert.equal(await manager.viewerHasGone(""), 0);
119
+ });
@@ -18,25 +18,42 @@ import { Viewer, viewerOf, viewersOf } from "../services/viewer/Viewer.js";
18
18
 
19
19
  const SESSION_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
20
20
 
21
- test("a viewer nobody has heard from is not watching", () => {
22
- const viewer = new Viewer("someone");
21
+ test("presence and position are two facts, and presence does not wait for a request", () => {
23
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);
24
41
 
25
- assert.equal(viewer.isLive(now, 60_000), false, "no head at all: they have never asked");
26
- viewer.head = { segment: 4, seconds: 16, at: now - 10_000 };
27
- assert.equal(viewer.isLive(now, 60_000), true);
28
- viewer.head = { segment: 4, seconds: 16, at: now - 120_000 };
29
- assert.equal(viewer.isLive(now, 60_000), false, "the tab is gone and nothing released it");
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");
30
47
  });
31
48
 
32
49
  test("a stated position outranks the segment they last asked for", () => {
33
50
  const viewer = new Viewer("someone");
34
51
  assert.equal(viewer.positionSeconds(), null);
35
52
 
36
- viewer.head = { segment: 10, seconds: 40, at: 1 };
53
+ viewer.position = { segment: 10, seconds: 40, at: 1 };
37
54
  assert.equal(viewer.positionSeconds(), 40, "where their player is reading");
38
55
 
39
- viewer.head = { segment: 10, seconds: 40, at: 1, seeked: 900 };
56
+ viewer.position = { segment: 10, seconds: 40, at: 1, seeked: 900 };
40
57
  assert.equal(viewer.positionSeconds(), 900, "a seek is the viewer saying where they are");
41
58
  });
42
59
 
@@ -80,7 +97,7 @@ test("releasing a consumer forgets everything that was true of them alone", asyn
80
97
  const leaving = viewerOf(session, "leaving");
81
98
  leaving.audio = { trackIndex: 2, transcode: true };
82
99
  leaving.activeVariantId = "some-variant";
83
- leaving.head = { segment: 40, seconds: 160, at: Date.now() };
100
+ leaving.position = { segment: 40, seconds: 160, at: Date.now() };
84
101
  viewerOf(session, "staying").audio = { trackIndex: 0, transcode: false };
85
102
 
86
103
  await manager.releaseSessionConsumer(SESSION_ID, "leaving", "the tab was closed");