@torrent-tv/proxy 2.87.11 → 2.87.12

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.
@@ -365,6 +365,8 @@ export class Viewer {
365
365
  * @param {number} [report.linkMbps] - What their link last measured. Absent
366
366
  * until something measurable has crossed it, and then the rest of the
367
367
  * report still stands: this is a statement about a viewer, not about a link.
368
+ * @param {number} [report.linkSampleAgeMs] - How old the newest raw link
369
+ * sample was when this report was made.
368
370
  * @param {number} report.bufferedAheadSec
369
371
  * @param {number} [report.bufferLimitSeconds] - The largest forward buffer
370
372
  * this browser accepted for the current player.
@@ -387,6 +389,7 @@ export class Viewer {
387
389
  linkMbps,
388
390
  linkSampleMbps = null,
389
391
  linkSampleAt = null,
392
+ linkSampleAgeMs = null,
390
393
  bufferedAheadSec,
391
394
  bufferLimitSeconds,
392
395
  positionSeconds = null,
@@ -425,7 +428,11 @@ export class Viewer {
425
428
  linkMeasuredAt,
426
429
  linkSampleAt: newerPlaybackSample ? sampleAt : this.netReport?.linkSampleAt ?? null,
427
430
  linkSampleMbps: newerPlaybackSample ? sampleMbps : this.netReport?.linkSampleMbps ?? null,
428
- linkSampleMeasuredAt: newerPlaybackSample ? now : this.netReport?.linkSampleMeasuredAt ?? null,
431
+ linkSampleMeasuredAt: newerPlaybackSample
432
+ ? Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0
433
+ ? now - linkSampleAgeMs
434
+ : now
435
+ : this.netReport?.linkSampleMeasuredAt ?? null,
429
436
  bufferedAheadSec: held,
430
437
  bufferLimitSeconds: Number.isFinite(bufferLimitSeconds) && bufferLimitSeconds > 0
431
438
  ? bufferLimitSeconds
@@ -1,212 +1,212 @@
1
- /**
2
- * @file One numbered list of soundtracks, from two readings of the same file.
3
- *
4
- * Two things are being pinned here, and each has cost a real failure elsewhere
5
- * in this project:
6
- *
7
- * 1. **The alignment guard.** The container reading is used ONLY when it can be
8
- * lined up with ffmpeg's own numbering, because `0:a:N` is what the encoder
9
- * is handed — a flag attributed to the wrong track is worse than a missing
10
- * one. Same discipline as `subtitle-defaults.js`, for the same reason.
11
- * 2. **The flat numbering.** The browser's menu, the `audioTrackIndex` on a
12
- * session request and the `a/<n>/` address of a rendition are one number.
13
- * Embedded tracks must keep the numbers they have always had, so that a
14
- * session created against an older cached plan means the same thing.
15
- */
16
-
17
- import test from "node:test";
18
- import { Container } from "../services/media/container/Container.js";
19
- import { AudioTrack } from "../services/media/tracks/AudioTrack.js";
20
- import assert from "node:assert/strict";
21
- import {
22
- audioRenditionName,
23
- buildAudioInventory,
24
- enrichAudioInventoryFromSidecar,
25
- resolveAudioIndex
26
- } from "../services/media/audio-inventory.js";
27
-
28
- test("a pair agreeing on language is accepted", () => {
29
- assert.equal(Container.pairingHolds({ language: "jpn" }, { language: "jpn" }), true);
30
- });
31
-
32
- test("a pair disagreeing on language is refused", () => {
33
- assert.equal(Container.pairingHolds({ language: "jpn" }, { language: "rus" }), false);
34
- });
35
-
36
- test("two tracks that say nothing about themselves do not break the alignment", () => {
37
- assert.equal(Container.pairingHolds({ language: "und", title: "" }, { language: "", name: "" }), true);
38
- });
39
-
40
- test("the container's own flags reach the merged track", () => {
41
- const merged = Container.mergeAudioFlags(
42
- [
43
- { index: 0, language: "eng", title: "", isDefault: true, codec: "aac" },
44
- { index: 1, language: "eng", title: "Director", isDefault: true, codec: "ac3" }
45
- ],
46
- [
47
- { language: "eng", name: "", isOriginal: true, isDefault: true, declaresDefault: true, channels: 6 },
48
- { language: "eng", name: "Director", isCommentary: true, isDefault: false, declaresDefault: true, channels: 2 }
49
- ]
50
- );
51
- assert.equal(merged.aligned, true);
52
- assert.equal(merged.tracks[0].isOriginal, true);
53
- assert.equal(merged.tracks[0].channels, 6);
54
- assert.equal(merged.tracks[1].isCommentary, true);
55
- // Matroska defaults FlagDefault to 1 and ffmpeg prints the applied default, so
56
- // the banner said both tracks were default. The container says otherwise.
57
- assert.equal(merged.tracks[1].isDefault, false);
58
- });
59
-
60
- test("a count that differs drops the container reading whole", () => {
61
- const merged = Container.mergeAudioFlags(
62
- [{ index: 0, language: "eng", isDefault: true }],
63
- [{ language: "eng" }, { language: "rus" }]
64
- );
65
- assert.equal(merged.aligned, false);
66
- assert.match(merged.reason, /declares 2 audio tracks and the probe found 1/);
67
- // Nothing of it is used — not even the flags that happened to line up.
68
- assert.equal(merged.tracks[0].isCommentary, false);
69
- assert.equal(merged.tracks[0].declaresDefault, false);
70
- });
71
-
72
- test("one pair that agrees on neither language nor title drops it too", () => {
73
- const merged = Container.mergeAudioFlags(
74
- [{ index: 0, language: "jpn", title: "" }, { index: 1, language: "rus", title: "" }],
75
- [{ language: "jpn", name: "" }, { language: "eng", name: "" }]
76
- );
77
- assert.equal(merged.aligned, false);
78
- assert.match(merged.reason, /audio 1 is/);
79
- });
80
-
81
- test("embedded tracks keep the numbers they have always had, sidecars follow", () => {
82
- const inventory = buildAudioInventory({
83
- embedded: [
84
- { language: "jpn", codec: "aac", isDefault: true },
85
- { language: "eng", codec: "ac3", title: "Commentary", isCommentary: true }
86
- ],
87
- videoFileIndex: 24,
88
- sidecars: [
89
- {
90
- file: { fileIndex: 12, name: "ep.mka", folders: ["Rus Sound"], extension: ".mka" },
91
- tracks: [{ language: "rus", codecId: "A_AC3", channels: 6 }]
92
- }
93
- ]
94
- });
95
-
96
- assert.deepEqual(inventory.map((entry) => entry.index), [0, 1, 2]);
97
- assert.deepEqual(inventory.map((entry) => entry.fileIndex), [24, 24, 12]);
98
- assert.deepEqual(inventory.map((entry) => entry.sourceTrackIndex), [0, 1, 0]);
99
- assert.deepEqual(inventory.map((entry) => entry.kind), ["embedded", "embedded", "sidecar"]);
100
- assert.equal(inventory[2].codec, "ac3");
101
- assert.deepEqual(inventory[2].folders, ["Rus Sound"]);
102
- assert.equal(inventory[2].fileName, "ep.mka");
103
- });
104
-
105
- test("late sidecar metadata enriches the existing entry without changing its address", () => {
106
- const file = { fileIndex: 12, name: "ep.mka", folders: ["Rus Sound"], extension: ".mka" };
107
- const inventory = buildAudioInventory({
108
- embedded: [{ language: "jpn", codec: "aac" }],
109
- videoFileIndex: 24,
110
- sidecars: [{ file, tracks: [] }]
111
- });
112
- const addresses = inventory.map(({ index, fileIndex, sourceTrackIndex, kind }) => ({
113
- index,
114
- fileIndex,
115
- sourceTrackIndex,
116
- kind
117
- }));
118
-
119
- assert.equal(enrichAudioInventoryFromSidecar(inventory, file, {
120
- language: "rus",
121
- languageBcp47: "ru",
122
- codecId: "A_AC3",
123
- channels: 6,
124
- isOriginal: true
125
- }), true);
126
-
127
- assert.deepEqual(inventory.map(({ index, fileIndex, sourceTrackIndex, kind }) => ({
128
- index,
129
- fileIndex,
130
- sourceTrackIndex,
131
- kind
132
- })), addresses);
133
- assert.equal(inventory.length, 2);
134
- assert.equal(inventory[1].codec, "ac3");
135
- assert.equal(inventory[1].languageBcp47, "ru");
136
- assert.equal(inventory[1].channels, 6);
137
- assert.equal(inventory[1].isOriginal, true);
138
- });
139
-
140
- test("a sidecar whose table could not be read is still offered, as one track", () => {
141
- const inventory = buildAudioInventory({
142
- embedded: [{ language: "eng", codec: "aac" }],
143
- videoFileIndex: 0,
144
- sidecars: [{ file: { fileIndex: 1, name: "dub.ac3", folders: [], extension: ".ac3" }, tracks: [] }]
145
- });
146
- assert.equal(inventory.length, 2);
147
- assert.equal(inventory[1].sourceTrackIndex, 0);
148
- // The extension of a bare elementary stream IS its codec.
149
- assert.equal(inventory[1].codec, "ac3");
150
- });
151
-
152
- test("a sidecar carrying two tracks contributes both", () => {
153
- const inventory = buildAudioInventory({
154
- embedded: [],
155
- videoFileIndex: 0,
156
- sidecars: [
157
- {
158
- file: { fileIndex: 3, name: "dubs.mka", folders: [], extension: ".mka" },
159
- tracks: [{ language: "rus" }, { language: "ukr" }]
160
- }
161
- ]
162
- });
163
- assert.deepEqual(inventory.map((entry) => [entry.index, entry.sourceTrackIndex]), [[0, 0], [1, 1]]);
164
- });
165
-
166
- test("the flat number resolves back to a file and a track inside it", () => {
167
- const inventory = buildAudioInventory({
168
- embedded: [{ language: "jpn" }],
169
- videoFileIndex: 24,
170
- sidecars: [{ file: { fileIndex: 12, name: "ep.mka", folders: [], extension: ".mka" }, tracks: [{}] }]
171
- });
172
- assert.equal(resolveAudioIndex(inventory, 1).fileIndex, 12);
173
- assert.equal(resolveAudioIndex(inventory, 1).sourceTrackIndex, 0);
174
- assert.equal(resolveAudioIndex(inventory, 9), null);
175
- });
176
-
177
- test("codec identifiers are translated to the names the browser judges by", () => {
178
- assert.equal(AudioTrack.codecNameOf({ codec: "AAC" }), "aac");
179
- assert.equal(AudioTrack.codecNameOf({ codecId: "A_AC3" }), "ac3");
180
- assert.equal(AudioTrack.codecNameOf({ codecId: "A_AAC/MPEG4/LC" }), "aac");
181
- assert.equal(AudioTrack.codecNameOf({ codecId: "A_PCM/INT/LIT" }), "pcm");
182
- assert.equal(AudioTrack.codecNameOf({ codecId: "ec-3" }), "eac3");
183
- assert.equal(AudioTrack.codecNameOf({}, ".dts"), "dts");
184
- assert.equal(AudioTrack.codecNameOf({}, ".unknown"), "");
185
- });
186
-
187
- test("a rendition is named by what the file says, and two never share a name", () => {
188
- const inventory = buildAudioInventory({
189
- embedded: [{ language: "jpn" }],
190
- videoFileIndex: 0,
191
- sidecars: [
192
- { file: { fileIndex: 1, name: "a.mka", folders: ["Rus Sound"], extension: ".mka" }, tracks: [{}] },
193
- { file: { fileIndex: 2, name: "b.mka", folders: ["Rus Sound"], extension: ".mka" }, tracks: [{}] }
194
- ]
195
- });
196
- assert.equal(audioRenditionName(inventory[0], inventory), "jpn");
197
- // Both sidecars sit in the same folder and neither names itself, so the names
198
- // would collide — and hls.js groups renditions by name.
199
- assert.notEqual(
200
- audioRenditionName(inventory[1], inventory),
201
- audioRenditionName(inventory[2], inventory)
202
- );
203
- });
204
-
205
- test("a commentary says so in its rendition name", () => {
206
- const inventory = buildAudioInventory({
207
- embedded: [{ language: "eng" }, { language: "eng", title: "Director", isCommentary: true }],
208
- videoFileIndex: 0,
209
- sidecars: []
210
- });
211
- assert.match(audioRenditionName(inventory[1], inventory), /commentary/);
212
- });
1
+ /**
2
+ * @file One numbered list of soundtracks, from two readings of the same file.
3
+ *
4
+ * Two things are being pinned here, and each has cost a real failure elsewhere
5
+ * in this project:
6
+ *
7
+ * 1. **The alignment guard.** The container reading is used ONLY when it can be
8
+ * lined up with ffmpeg's own numbering, because `0:a:N` is what the encoder
9
+ * is handed — a flag attributed to the wrong track is worse than a missing
10
+ * one. Same discipline as `subtitle-defaults.js`, for the same reason.
11
+ * 2. **The flat numbering.** The browser's menu, the `audioTrackIndex` on a
12
+ * session request and the `a/<n>/` address of a rendition are one number.
13
+ * Embedded tracks must keep the numbers they have always had, so that a
14
+ * session created against an older cached plan means the same thing.
15
+ */
16
+
17
+ import test from "node:test";
18
+ import { Container } from "../services/media/container/Container.js";
19
+ import { AudioTrack } from "../services/media/tracks/AudioTrack.js";
20
+ import assert from "node:assert/strict";
21
+ import {
22
+ audioRenditionName,
23
+ buildAudioInventory,
24
+ enrichAudioInventoryFromSidecar,
25
+ resolveAudioIndex
26
+ } from "../services/media/audio-inventory.js";
27
+
28
+ test("a pair agreeing on language is accepted", () => {
29
+ assert.equal(Container.pairingHolds({ language: "jpn" }, { language: "jpn" }), true);
30
+ });
31
+
32
+ test("a pair disagreeing on language is refused", () => {
33
+ assert.equal(Container.pairingHolds({ language: "jpn" }, { language: "rus" }), false);
34
+ });
35
+
36
+ test("two tracks that say nothing about themselves do not break the alignment", () => {
37
+ assert.equal(Container.pairingHolds({ language: "und", title: "" }, { language: "", name: "" }), true);
38
+ });
39
+
40
+ test("the container's own flags reach the merged track", () => {
41
+ const merged = Container.mergeAudioFlags(
42
+ [
43
+ { index: 0, language: "eng", title: "", isDefault: true, codec: "aac" },
44
+ { index: 1, language: "eng", title: "Director", isDefault: true, codec: "ac3" }
45
+ ],
46
+ [
47
+ { language: "eng", name: "", isOriginal: true, isDefault: true, declaresDefault: true, channels: 6 },
48
+ { language: "eng", name: "Director", isCommentary: true, isDefault: false, declaresDefault: true, channels: 2 }
49
+ ]
50
+ );
51
+ assert.equal(merged.aligned, true);
52
+ assert.equal(merged.tracks[0].isOriginal, true);
53
+ assert.equal(merged.tracks[0].channels, 6);
54
+ assert.equal(merged.tracks[1].isCommentary, true);
55
+ // Matroska defaults FlagDefault to 1 and ffmpeg prints the applied default, so
56
+ // the banner said both tracks were default. The container says otherwise.
57
+ assert.equal(merged.tracks[1].isDefault, false);
58
+ });
59
+
60
+ test("a count that differs drops the container reading whole", () => {
61
+ const merged = Container.mergeAudioFlags(
62
+ [{ index: 0, language: "eng", isDefault: true }],
63
+ [{ language: "eng" }, { language: "rus" }]
64
+ );
65
+ assert.equal(merged.aligned, false);
66
+ assert.match(merged.reason, /declares 2 audio tracks and the probe found 1/);
67
+ // Nothing of it is used — not even the flags that happened to line up.
68
+ assert.equal(merged.tracks[0].isCommentary, false);
69
+ assert.equal(merged.tracks[0].declaresDefault, false);
70
+ });
71
+
72
+ test("one pair that agrees on neither language nor title drops it too", () => {
73
+ const merged = Container.mergeAudioFlags(
74
+ [{ index: 0, language: "jpn", title: "" }, { index: 1, language: "rus", title: "" }],
75
+ [{ language: "jpn", name: "" }, { language: "eng", name: "" }]
76
+ );
77
+ assert.equal(merged.aligned, false);
78
+ assert.match(merged.reason, /audio 1 is/);
79
+ });
80
+
81
+ test("embedded tracks keep the numbers they have always had, sidecars follow", () => {
82
+ const inventory = buildAudioInventory({
83
+ embedded: [
84
+ { language: "jpn", codec: "aac", isDefault: true },
85
+ { language: "eng", codec: "ac3", title: "Commentary", isCommentary: true }
86
+ ],
87
+ videoFileIndex: 24,
88
+ sidecars: [
89
+ {
90
+ file: { fileIndex: 12, name: "ep.mka", folders: ["Rus Sound"], extension: ".mka" },
91
+ tracks: [{ language: "rus", codecId: "A_AC3", channels: 6 }]
92
+ }
93
+ ]
94
+ });
95
+
96
+ assert.deepEqual(inventory.map((entry) => entry.index), [0, 1, 2]);
97
+ assert.deepEqual(inventory.map((entry) => entry.fileIndex), [24, 24, 12]);
98
+ assert.deepEqual(inventory.map((entry) => entry.sourceTrackIndex), [0, 1, 0]);
99
+ assert.deepEqual(inventory.map((entry) => entry.kind), ["embedded", "embedded", "sidecar"]);
100
+ assert.equal(inventory[2].codec, "ac3");
101
+ assert.deepEqual(inventory[2].folders, ["Rus Sound"]);
102
+ assert.equal(inventory[2].fileName, "ep.mka");
103
+ });
104
+
105
+ test("late sidecar metadata enriches the existing entry without changing its address", () => {
106
+ const file = { fileIndex: 12, name: "ep.mka", folders: ["Rus Sound"], extension: ".mka" };
107
+ const inventory = buildAudioInventory({
108
+ embedded: [{ language: "jpn", codec: "aac" }],
109
+ videoFileIndex: 24,
110
+ sidecars: [{ file, tracks: [] }]
111
+ });
112
+ const addresses = inventory.map(({ index, fileIndex, sourceTrackIndex, kind }) => ({
113
+ index,
114
+ fileIndex,
115
+ sourceTrackIndex,
116
+ kind
117
+ }));
118
+
119
+ assert.equal(enrichAudioInventoryFromSidecar(inventory, file, {
120
+ language: "rus",
121
+ languageBcp47: "ru",
122
+ codecId: "A_AC3",
123
+ channels: 6,
124
+ isOriginal: true
125
+ }), true);
126
+
127
+ assert.deepEqual(inventory.map(({ index, fileIndex, sourceTrackIndex, kind }) => ({
128
+ index,
129
+ fileIndex,
130
+ sourceTrackIndex,
131
+ kind
132
+ })), addresses);
133
+ assert.equal(inventory.length, 2);
134
+ assert.equal(inventory[1].codec, "ac3");
135
+ assert.equal(inventory[1].languageBcp47, "ru");
136
+ assert.equal(inventory[1].channels, 6);
137
+ assert.equal(inventory[1].isOriginal, true);
138
+ });
139
+
140
+ test("a sidecar whose table could not be read is still offered, as one track", () => {
141
+ const inventory = buildAudioInventory({
142
+ embedded: [{ language: "eng", codec: "aac" }],
143
+ videoFileIndex: 0,
144
+ sidecars: [{ file: { fileIndex: 1, name: "dub.ac3", folders: [], extension: ".ac3" }, tracks: [] }]
145
+ });
146
+ assert.equal(inventory.length, 2);
147
+ assert.equal(inventory[1].sourceTrackIndex, 0);
148
+ // The extension of a bare elementary stream IS its codec.
149
+ assert.equal(inventory[1].codec, "ac3");
150
+ });
151
+
152
+ test("a sidecar carrying two tracks contributes both", () => {
153
+ const inventory = buildAudioInventory({
154
+ embedded: [],
155
+ videoFileIndex: 0,
156
+ sidecars: [
157
+ {
158
+ file: { fileIndex: 3, name: "dubs.mka", folders: [], extension: ".mka" },
159
+ tracks: [{ language: "rus" }, { language: "ukr" }]
160
+ }
161
+ ]
162
+ });
163
+ assert.deepEqual(inventory.map((entry) => [entry.index, entry.sourceTrackIndex]), [[0, 0], [1, 1]]);
164
+ });
165
+
166
+ test("the flat number resolves back to a file and a track inside it", () => {
167
+ const inventory = buildAudioInventory({
168
+ embedded: [{ language: "jpn" }],
169
+ videoFileIndex: 24,
170
+ sidecars: [{ file: { fileIndex: 12, name: "ep.mka", folders: [], extension: ".mka" }, tracks: [{}] }]
171
+ });
172
+ assert.equal(resolveAudioIndex(inventory, 1).fileIndex, 12);
173
+ assert.equal(resolveAudioIndex(inventory, 1).sourceTrackIndex, 0);
174
+ assert.equal(resolveAudioIndex(inventory, 9), null);
175
+ });
176
+
177
+ test("codec identifiers are translated to the names the browser judges by", () => {
178
+ assert.equal(AudioTrack.codecNameOf({ codec: "AAC" }), "aac");
179
+ assert.equal(AudioTrack.codecNameOf({ codecId: "A_AC3" }), "ac3");
180
+ assert.equal(AudioTrack.codecNameOf({ codecId: "A_AAC/MPEG4/LC" }), "aac");
181
+ assert.equal(AudioTrack.codecNameOf({ codecId: "A_PCM/INT/LIT" }), "pcm");
182
+ assert.equal(AudioTrack.codecNameOf({ codecId: "ec-3" }), "eac3");
183
+ assert.equal(AudioTrack.codecNameOf({}, ".dts"), "dts");
184
+ assert.equal(AudioTrack.codecNameOf({}, ".unknown"), "");
185
+ });
186
+
187
+ test("a rendition is named by what the file says, and two never share a name", () => {
188
+ const inventory = buildAudioInventory({
189
+ embedded: [{ language: "jpn" }],
190
+ videoFileIndex: 0,
191
+ sidecars: [
192
+ { file: { fileIndex: 1, name: "a.mka", folders: ["Rus Sound"], extension: ".mka" }, tracks: [{}] },
193
+ { file: { fileIndex: 2, name: "b.mka", folders: ["Rus Sound"], extension: ".mka" }, tracks: [{}] }
194
+ ]
195
+ });
196
+ assert.equal(audioRenditionName(inventory[0], inventory), "jpn");
197
+ // Both sidecars sit in the same folder and neither names itself, so the names
198
+ // would collide — and hls.js groups renditions by name.
199
+ assert.notEqual(
200
+ audioRenditionName(inventory[1], inventory),
201
+ audioRenditionName(inventory[2], inventory)
202
+ );
203
+ });
204
+
205
+ test("a commentary says so in its rendition name", () => {
206
+ const inventory = buildAudioInventory({
207
+ embedded: [{ language: "eng" }, { language: "eng", title: "Director", isCommentary: true }],
208
+ videoFileIndex: 0,
209
+ sidecars: []
210
+ });
211
+ assert.match(audioRenditionName(inventory[1], inventory), /commentary/);
212
+ });
@@ -1,92 +1,92 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { createDataChannelHandler } from "../services/transport/data-channel-handler.js";
4
-
5
- class FakeDataChannel {
6
- constructor(label) {
7
- this.label = label;
8
- this.messages = [];
9
- this.closed = null;
10
- }
11
-
12
- getLabel() {
13
- return this.label;
14
- }
15
-
16
- onMessage(callback) {
17
- this.message = callback;
18
- }
19
-
20
- onClosed(callback) {
21
- this.closed = callback;
22
- }
23
-
24
- onError() {}
25
-
26
- sendMessage(message) {
27
- this.messages.push(message);
28
- }
29
-
30
- close() {
31
- this.closed?.();
32
- }
33
- }
34
-
35
- test("a viewer remains present until every data channel on the connection closes", () => {
36
- const gone = [];
37
- const handler = createDataChannelHandler({
38
- proxyPort: 9090,
39
- getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
40
- onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
41
- });
42
- const media = new FakeDataChannel("proxy");
43
- const control = new FakeDataChannel("proxy-control");
44
- const probe = new FakeDataChannel("proxy-fast");
45
-
46
- handler.handleChannel("peer-session", media);
47
- handler.handleChannel("peer-session", control);
48
- handler.handleChannel("peer-session", probe);
49
- control.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
50
-
51
- control.close();
52
- assert.deepEqual(gone, [], "closing the control channel alone does not release the viewer");
53
-
54
- media.close();
55
- assert.deepEqual(gone, [], "the viewer remains while the probe channel is still open");
56
-
57
- probe.close();
58
- assert.deepEqual(gone, [
59
- { consumerId: "viewer-1", reason: "the connection closed" }
60
- ]);
61
- });
62
-
63
- test("a closing connection does not release a viewer already named on its replacement", () => {
64
- const gone = [];
65
- const handler = createDataChannelHandler({
66
- proxyPort: 9090,
67
- getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
68
- onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
69
- });
70
- const oldControl = new FakeDataChannel("proxy-control");
71
- const oldMedia = new FakeDataChannel("proxy");
72
- const newControl = new FakeDataChannel("proxy-control");
73
- const newMedia = new FakeDataChannel("proxy");
74
-
75
- handler.handleChannel("old-peer", oldControl);
76
- handler.handleChannel("old-peer", oldMedia);
77
- oldControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
78
- handler.handleChannel("new-peer", newControl);
79
- handler.handleChannel("new-peer", newMedia);
80
- newControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
81
-
82
- oldControl.close();
83
- oldMedia.close();
84
- assert.deepEqual(gone, [], "the old connection no longer owns the viewer");
85
-
86
- newControl.close();
87
- assert.deepEqual(gone, [], "the replacement still has its media channel");
88
- newMedia.close();
89
- assert.deepEqual(gone, [
90
- { consumerId: "viewer-1", reason: "the connection closed" }
91
- ]);
92
- });
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createDataChannelHandler } from "../services/transport/data-channel-handler.js";
4
+
5
+ class FakeDataChannel {
6
+ constructor(label) {
7
+ this.label = label;
8
+ this.messages = [];
9
+ this.closed = null;
10
+ }
11
+
12
+ getLabel() {
13
+ return this.label;
14
+ }
15
+
16
+ onMessage(callback) {
17
+ this.message = callback;
18
+ }
19
+
20
+ onClosed(callback) {
21
+ this.closed = callback;
22
+ }
23
+
24
+ onError() {}
25
+
26
+ sendMessage(message) {
27
+ this.messages.push(message);
28
+ }
29
+
30
+ close() {
31
+ this.closed?.();
32
+ }
33
+ }
34
+
35
+ test("a viewer remains present until every data channel on the connection closes", () => {
36
+ const gone = [];
37
+ const handler = createDataChannelHandler({
38
+ proxyPort: 9090,
39
+ getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
40
+ onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
41
+ });
42
+ const media = new FakeDataChannel("proxy");
43
+ const control = new FakeDataChannel("proxy-control");
44
+ const probe = new FakeDataChannel("proxy-fast");
45
+
46
+ handler.handleChannel("peer-session", media);
47
+ handler.handleChannel("peer-session", control);
48
+ handler.handleChannel("peer-session", probe);
49
+ control.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
50
+
51
+ control.close();
52
+ assert.deepEqual(gone, [], "closing the control channel alone does not release the viewer");
53
+
54
+ media.close();
55
+ assert.deepEqual(gone, [], "the viewer remains while the probe channel is still open");
56
+
57
+ probe.close();
58
+ assert.deepEqual(gone, [
59
+ { consumerId: "viewer-1", reason: "the connection closed" }
60
+ ]);
61
+ });
62
+
63
+ test("a closing connection does not release a viewer already named on its replacement", () => {
64
+ const gone = [];
65
+ const handler = createDataChannelHandler({
66
+ proxyPort: 9090,
67
+ getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
68
+ onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
69
+ });
70
+ const oldControl = new FakeDataChannel("proxy-control");
71
+ const oldMedia = new FakeDataChannel("proxy");
72
+ const newControl = new FakeDataChannel("proxy-control");
73
+ const newMedia = new FakeDataChannel("proxy");
74
+
75
+ handler.handleChannel("old-peer", oldControl);
76
+ handler.handleChannel("old-peer", oldMedia);
77
+ oldControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
78
+ handler.handleChannel("new-peer", newControl);
79
+ handler.handleChannel("new-peer", newMedia);
80
+ newControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
81
+
82
+ oldControl.close();
83
+ oldMedia.close();
84
+ assert.deepEqual(gone, [], "the old connection no longer owns the viewer");
85
+
86
+ newControl.close();
87
+ assert.deepEqual(gone, [], "the replacement still has its media channel");
88
+ newMedia.close();
89
+ assert.deepEqual(gone, [
90
+ { consumerId: "viewer-1", reason: "the connection closed" }
91
+ ]);
92
+ });