@torrent-tv/proxy 2.87.6 → 2.87.8

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,176 +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
- resolveAudioIndex
25
- } from "../services/media/audio-inventory.js";
26
-
27
- test("a pair agreeing on language is accepted", () => {
28
- assert.equal(Container.pairingHolds({ language: "jpn" }, { language: "jpn" }), true);
29
- });
30
-
31
- test("a pair disagreeing on language is refused", () => {
32
- assert.equal(Container.pairingHolds({ language: "jpn" }, { language: "rus" }), false);
33
- });
34
-
35
- test("two tracks that say nothing about themselves do not break the alignment", () => {
36
- assert.equal(Container.pairingHolds({ language: "und", title: "" }, { language: "", name: "" }), true);
37
- });
38
-
39
- test("the container's own flags reach the merged track", () => {
40
- const merged = Container.mergeAudioFlags(
41
- [
42
- { index: 0, language: "eng", title: "", isDefault: true, codec: "aac" },
43
- { index: 1, language: "eng", title: "Director", isDefault: true, codec: "ac3" }
44
- ],
45
- [
46
- { language: "eng", name: "", isOriginal: true, isDefault: true, declaresDefault: true, channels: 6 },
47
- { language: "eng", name: "Director", isCommentary: true, isDefault: false, declaresDefault: true, channels: 2 }
48
- ]
49
- );
50
- assert.equal(merged.aligned, true);
51
- assert.equal(merged.tracks[0].isOriginal, true);
52
- assert.equal(merged.tracks[0].channels, 6);
53
- assert.equal(merged.tracks[1].isCommentary, true);
54
- // Matroska defaults FlagDefault to 1 and ffmpeg prints the applied default, so
55
- // the banner said both tracks were default. The container says otherwise.
56
- assert.equal(merged.tracks[1].isDefault, false);
57
- });
58
-
59
- test("a count that differs drops the container reading whole", () => {
60
- const merged = Container.mergeAudioFlags(
61
- [{ index: 0, language: "eng", isDefault: true }],
62
- [{ language: "eng" }, { language: "rus" }]
63
- );
64
- assert.equal(merged.aligned, false);
65
- assert.match(merged.reason, /declares 2 audio tracks and the probe found 1/);
66
- // Nothing of it is used — not even the flags that happened to line up.
67
- assert.equal(merged.tracks[0].isCommentary, false);
68
- assert.equal(merged.tracks[0].declaresDefault, false);
69
- });
70
-
71
- test("one pair that agrees on neither language nor title drops it too", () => {
72
- const merged = Container.mergeAudioFlags(
73
- [{ index: 0, language: "jpn", title: "" }, { index: 1, language: "rus", title: "" }],
74
- [{ language: "jpn", name: "" }, { language: "eng", name: "" }]
75
- );
76
- assert.equal(merged.aligned, false);
77
- assert.match(merged.reason, /audio 1 is/);
78
- });
79
-
80
- test("embedded tracks keep the numbers they have always had, sidecars follow", () => {
81
- const inventory = buildAudioInventory({
82
- embedded: [
83
- { language: "jpn", codec: "aac", isDefault: true },
84
- { language: "eng", codec: "ac3", title: "Commentary", isCommentary: true }
85
- ],
86
- videoFileIndex: 24,
87
- sidecars: [
88
- {
89
- file: { fileIndex: 12, name: "ep.mka", folders: ["Rus Sound"], extension: ".mka" },
90
- tracks: [{ language: "rus", codecId: "A_AC3", channels: 6 }]
91
- }
92
- ]
93
- });
94
-
95
- assert.deepEqual(inventory.map((entry) => entry.index), [0, 1, 2]);
96
- assert.deepEqual(inventory.map((entry) => entry.fileIndex), [24, 24, 12]);
97
- assert.deepEqual(inventory.map((entry) => entry.sourceTrackIndex), [0, 1, 0]);
98
- assert.deepEqual(inventory.map((entry) => entry.kind), ["embedded", "embedded", "sidecar"]);
99
- assert.equal(inventory[2].codec, "ac3");
100
- assert.deepEqual(inventory[2].folders, ["Rus Sound"]);
101
- assert.equal(inventory[2].fileName, "ep.mka");
102
- });
103
-
104
- test("a sidecar whose table could not be read is still offered, as one track", () => {
105
- const inventory = buildAudioInventory({
106
- embedded: [{ language: "eng", codec: "aac" }],
107
- videoFileIndex: 0,
108
- sidecars: [{ file: { fileIndex: 1, name: "dub.ac3", folders: [], extension: ".ac3" }, tracks: [] }]
109
- });
110
- assert.equal(inventory.length, 2);
111
- assert.equal(inventory[1].sourceTrackIndex, 0);
112
- // The extension of a bare elementary stream IS its codec.
113
- assert.equal(inventory[1].codec, "ac3");
114
- });
115
-
116
- test("a sidecar carrying two tracks contributes both", () => {
117
- const inventory = buildAudioInventory({
118
- embedded: [],
119
- videoFileIndex: 0,
120
- sidecars: [
121
- {
122
- file: { fileIndex: 3, name: "dubs.mka", folders: [], extension: ".mka" },
123
- tracks: [{ language: "rus" }, { language: "ukr" }]
124
- }
125
- ]
126
- });
127
- assert.deepEqual(inventory.map((entry) => [entry.index, entry.sourceTrackIndex]), [[0, 0], [1, 1]]);
128
- });
129
-
130
- test("the flat number resolves back to a file and a track inside it", () => {
131
- const inventory = buildAudioInventory({
132
- embedded: [{ language: "jpn" }],
133
- videoFileIndex: 24,
134
- sidecars: [{ file: { fileIndex: 12, name: "ep.mka", folders: [], extension: ".mka" }, tracks: [{}] }]
135
- });
136
- assert.equal(resolveAudioIndex(inventory, 1).fileIndex, 12);
137
- assert.equal(resolveAudioIndex(inventory, 1).sourceTrackIndex, 0);
138
- assert.equal(resolveAudioIndex(inventory, 9), null);
139
- });
140
-
141
- test("codec identifiers are translated to the names the browser judges by", () => {
142
- assert.equal(AudioTrack.codecNameOf({ codec: "AAC" }), "aac");
143
- assert.equal(AudioTrack.codecNameOf({ codecId: "A_AC3" }), "ac3");
144
- assert.equal(AudioTrack.codecNameOf({ codecId: "A_AAC/MPEG4/LC" }), "aac");
145
- assert.equal(AudioTrack.codecNameOf({ codecId: "A_PCM/INT/LIT" }), "pcm");
146
- assert.equal(AudioTrack.codecNameOf({ codecId: "ec-3" }), "eac3");
147
- assert.equal(AudioTrack.codecNameOf({}, ".dts"), "dts");
148
- assert.equal(AudioTrack.codecNameOf({}, ".unknown"), "");
149
- });
150
-
151
- test("a rendition is named by what the file says, and two never share a name", () => {
152
- const inventory = buildAudioInventory({
153
- embedded: [{ language: "jpn" }],
154
- videoFileIndex: 0,
155
- sidecars: [
156
- { file: { fileIndex: 1, name: "a.mka", folders: ["Rus Sound"], extension: ".mka" }, tracks: [{}] },
157
- { file: { fileIndex: 2, name: "b.mka", folders: ["Rus Sound"], extension: ".mka" }, tracks: [{}] }
158
- ]
159
- });
160
- assert.equal(audioRenditionName(inventory[0], inventory), "jpn");
161
- // Both sidecars sit in the same folder and neither names itself, so the names
162
- // would collide — and hls.js groups renditions by name.
163
- assert.notEqual(
164
- audioRenditionName(inventory[1], inventory),
165
- audioRenditionName(inventory[2], inventory)
166
- );
167
- });
168
-
169
- test("a commentary says so in its rendition name", () => {
170
- const inventory = buildAudioInventory({
171
- embedded: [{ language: "eng" }, { language: "eng", title: "Director", isCommentary: true }],
172
- videoFileIndex: 0,
173
- sidecars: []
174
- });
175
- assert.match(audioRenditionName(inventory[1], inventory), /commentary/);
176
- });
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
+ });
@@ -0,0 +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
+ });