@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,212 +1,245 @@
1
- /**
2
- * The soundtracks a viewer may choose between, as ONE numbered list — the ones
3
- * muxed into the picture and the ones shipped as separate files beside it.
4
- *
5
- * Why one list. Everything downstream addresses a soundtrack by a single number:
6
- * the browser's menu, the `audioTrackIndex` on the session-create request, the
7
- * `a/<n>/` path a rendition is published at, and hls.js's own rendition order.
8
- * Giving a sidecar file its own numbering would mean a second vocabulary and a
9
- * translation at every boundary. Instead the number stays flat and this module
10
- * owns the only place that knows what it resolves to: which FILE the track lives
11
- * in, and which track it is inside that file.
12
- *
13
- * The list is built from two readings of the same file, and that is deliberate:
14
- *
15
- * - ffmpeg's `-i` banner, which is what `0:a:N` will select and therefore the
16
- * authority on NUMBERING;
17
- * - the container's own track table (`services/container/`), which is the only
18
- * authority on the FLAGS — `FlagOriginal`, `FlagCommentary`,
19
- * `FlagVisualImpaired`, `FlagEnabled` and `LanguageBCP47` do not appear in the
20
- * banner at all, so the audio menu could not tell a director's commentary from
21
- * the film itself.
22
- *
23
- * The two are lined up by position and the pairing is CHECKED, exactly as
24
- * `subtitle-defaults.js` checks its own: a length that differs, or one pair that
25
- * agrees on neither language nor title, means the two readings are not
26
- * describing the same thing in the same order — and then the container reading
27
- * is dropped whole rather than attributed to the wrong track. A wrong flag is
28
- * worse than a missing one, because `0:a:N` is what the encoder is given.
29
- */
30
-
31
-
32
- import { AudioTrack } from "./tracks/AudioTrack.js";
33
-
34
-
35
-
36
-
37
-
38
- /**
39
- * @typedef {object} AudioInventoryEntry
40
- * @property {number} index - The flat number everything downstream uses.
41
- * @property {number} fileIndex - The torrent file this track lives in.
42
- * @property {number} sourceTrackIndex - `0:a:N` WITHIN that file.
43
- * @property {"embedded" | "sidecar"} kind - Whether it is muxed into the picture
44
- * or ships as a file beside it. Not a type of track — a statement about where
45
- * the bytes are.
46
- * @property {string} codec
47
- * @property {string} language - As the container states it, or "" when it does
48
- * not. Never guessed here: what a folder name suggests is derived in the
49
- * browser, where the language table and the viewer's locale already live.
50
- * @property {string} languageBcp47
51
- * @property {string} title
52
- * @property {boolean} isDefault
53
- * @property {boolean} declaresDefault
54
- * @property {boolean} isOriginal
55
- * @property {boolean} isCommentary
56
- * @property {boolean} isVisualImpaired
57
- * @property {boolean} isEnabled
58
- * @property {number | null} channels
59
- * @property {number | null} bitrateKbps - The rate the file states for this track; an average, not a bound.
60
- * @property {string} fileName - For a sidecar: its own file name. "" otherwise.
61
- * @property {string[]} folders - For a sidecar: the folders above it, relative
62
- * to the torrent root. What the browser reads a language and a releaser from.
63
- */
64
-
65
- /**
66
- * One numbered list from the picture's own tracks and its sidecar files.
67
- *
68
- * Order is load-bearing: embedded tracks keep the numbers they have always had,
69
- * so a session created before this existed and one created after agree about
70
- * what `audioTrackIndex: 1` means, and sidecars are appended after them in
71
- * torrent-file order. Sidecar files are stable in that order for a given
72
- * torrent, so the numbering is stable for a given file.
73
- *
74
- * @param {object} params
75
- * @param {object[]} params.embedded - Merged banner+container tracks of the video.
76
- * @param {number} params.videoFileIndex
77
- * @param {Array<{ file: import("../torrent/files.js").SidecarFile, tracks: object[] }>} params.sidecars
78
- * Each sidecar file with the audio tracks IT holds. A file whose container
79
- * could not be read contributes one track, which is what a bare elementary
80
- * stream is.
81
- * @returns {AudioInventoryEntry[]}
82
- */
83
- export function buildAudioInventory({ embedded, videoFileIndex, sidecars }) {
84
- /** @type {AudioInventoryEntry[]} */
85
- const inventory = [];
86
- const add = (track, fileIndex, sourceTrackIndex, kind, file) => {
87
- inventory.push({
88
- index: inventory.length,
89
- fileIndex,
90
- sourceTrackIndex,
91
- kind,
92
- // The name the browser judges "can I play this?" by. For an embedded
93
- // track it is ffmpeg's own; for a sidecar it is translated from what the
94
- // container wrote, or from the extension when there was no table to read.
95
- codec: AudioTrack.codecNameOf(track, file?.extension ?? ""),
96
- language: typeof track?.language === "string" ? track.language : "",
97
- languageBcp47: typeof track?.languageBcp47 === "string" ? track.languageBcp47 : "",
98
- title:
99
- typeof track?.title === "string" && track.title.length > 0
100
- ? track.title
101
- : (typeof track?.name === "string" ? track.name : ""),
102
- isDefault: track?.isDefault === true,
103
- declaresDefault: track?.declaresDefault === true,
104
- isOriginal: track?.isOriginal === true,
105
- isCommentary: track?.isCommentary === true,
106
- isVisualImpaired: track?.isVisualImpaired === true,
107
- isEnabled: track?.isEnabled !== false,
108
- channels: Number.isFinite(track?.channels) ? track.channels : null,
109
- // The rate the file STATES for this track, in kbit/s — an average, not a
110
- // bound — or null where nothing states one (a sidecar read from its
111
- // container table, a Matroska stream without statistics tags). What a
112
- // viewer's link is asked to carry for a copied soundtrack is this.
113
- bitrateKbps: Number.isFinite(track?.bitrateKbps) && track.bitrateKbps > 0 ? track.bitrateKbps : null,
114
- fileName: kind === "sidecar" ? (file?.name ?? "") : "",
115
- folders: kind === "sidecar" && Array.isArray(file?.folders) ? file.folders : [],
116
- // What the file's own path says, for a track that ships as its own file:
117
- // read here, by the grammar that also decided this file belongs to this
118
- // picture. The browser used to read the same path again to build the
119
- // label, which is one name read twice by two rules.
120
- naming: kind === "sidecar" ? (file?.naming ?? null) : null
121
- });
122
- };
123
-
124
- for (const [order, track] of (Array.isArray(embedded) ? embedded : []).entries()) {
125
- add(track, videoFileIndex, order, "embedded", null);
126
- }
127
- for (const sidecar of Array.isArray(sidecars) ? sidecars : []) {
128
- const tracks = Array.isArray(sidecar?.tracks) && sidecar.tracks.length > 0
129
- ? sidecar.tracks
130
- // A file whose track table could not be read is one track: `.ac3`, `.dts`
131
- // and `.mp3` have no table to read, and a `.mka` whose head has not
132
- // arrived yet is better offered than hidden — ffmpeg will find its first
133
- // audio stream either way.
134
- : [{}];
135
- for (const [order, track] of tracks.entries()) {
136
- add(track, sidecar.file.fileIndex, order, "sidecar", sidecar.file);
137
- }
138
- }
139
- return inventory;
140
- }
141
-
142
- /**
143
- * Resolve the flat number back to the file and the track inside it.
144
- *
145
- * @param {AudioInventoryEntry[]} inventory
146
- * @param {number} index
147
- * @returns {AudioInventoryEntry | null}
148
- */
149
- export function resolveAudioIndex(inventory, index) {
150
- if (!Array.isArray(inventory) || !Number.isInteger(index) || index < 0) {
151
- return null;
152
- }
153
- return inventory.find((entry) => entry.index === index) ?? null;
154
- }
155
-
156
- /**
157
- * The name an `#EXT-X-MEDIA` line carries for one soundtrack.
158
- *
159
- * Deliberately plain, and deliberately NOT localised: this is the name inside a
160
- * playlist, and what the viewer reads in the menu is composed in the browser
161
- * from the same facts, where the language table and the viewer's own locale are.
162
- * The only requirements here are that it says something and that no two
163
- * renditions of one file share it — hls.js groups renditions by name.
164
- *
165
- * @param {AudioInventoryEntry} entry
166
- * @param {AudioInventoryEntry[]} inventory
167
- * @returns {string}
168
- */
169
- export function audioRenditionName(entry, inventory) {
170
- const parts = [];
171
- if (entry.title) {
172
- parts.push(entry.title);
173
- } else if (entry.languageBcp47 || entry.language) {
174
- parts.push(entry.languageBcp47 || entry.language);
175
- } else if (entry.folders.length > 0) {
176
- // The folder a dub sits in is usually the only thing naming it, and a
177
- // release names it for a reason: "Rus Sound", "Ukr Dub".
178
- parts.push(entry.folders[entry.folders.length - 1]);
179
- } else if (entry.fileName) {
180
- parts.push(entry.fileName);
181
- } else {
182
- parts.push(`Track ${entry.index + 1}`);
183
- }
184
- if (entry.isCommentary) {
185
- parts.push("commentary");
186
- } else if (entry.isVisualImpaired) {
187
- parts.push("described");
188
- }
189
- const name = parts.join(" · ");
190
- const clash = (Array.isArray(inventory) ? inventory : []).some(
191
- (other) => other.index !== entry.index && audioRenditionNameCore(other) === audioRenditionNameCore(entry)
192
- );
193
- return clash ? `${name} (${entry.index + 1})` : name;
194
- }
195
-
196
- /**
197
- * The part of a rendition name that a clash is judged on — the name without the
198
- * disambiguating number, so that adding the number cannot itself cause a clash.
199
- *
200
- * @param {AudioInventoryEntry} entry
201
- * @returns {string}
202
- */
203
- function audioRenditionNameCore(entry) {
204
- return (
205
- entry.title ||
206
- entry.languageBcp47 ||
207
- entry.language ||
208
- (entry.folders.length > 0 ? entry.folders[entry.folders.length - 1] : "") ||
209
- entry.fileName ||
210
- ""
211
- );
212
- }
1
+ /**
2
+ * The soundtracks a viewer may choose between, as ONE numbered list — the ones
3
+ * muxed into the picture and the ones shipped as separate files beside it.
4
+ *
5
+ * Why one list. Everything downstream addresses a soundtrack by a single number:
6
+ * the browser's menu, the `audioTrackIndex` on the session-create request, the
7
+ * `a/<n>/` path a rendition is published at, and hls.js's own rendition order.
8
+ * Giving a sidecar file its own numbering would mean a second vocabulary and a
9
+ * translation at every boundary. Instead the number stays flat and this module
10
+ * owns the only place that knows what it resolves to: which FILE the track lives
11
+ * in, and which track it is inside that file.
12
+ *
13
+ * The list is built from two readings of the same file, and that is deliberate:
14
+ *
15
+ * - ffmpeg's `-i` banner, which is what `0:a:N` will select and therefore the
16
+ * authority on NUMBERING;
17
+ * - the container's own track table (`services/container/`), which is the only
18
+ * authority on the FLAGS — `FlagOriginal`, `FlagCommentary`,
19
+ * `FlagVisualImpaired`, `FlagEnabled` and `LanguageBCP47` do not appear in the
20
+ * banner at all, so the audio menu could not tell a director's commentary from
21
+ * the film itself.
22
+ *
23
+ * The two are lined up by position and the pairing is CHECKED, exactly as
24
+ * `subtitle-defaults.js` checks its own: a length that differs, or one pair that
25
+ * agrees on neither language nor title, means the two readings are not
26
+ * describing the same thing in the same order — and then the container reading
27
+ * is dropped whole rather than attributed to the wrong track. A wrong flag is
28
+ * worse than a missing one, because `0:a:N` is what the encoder is given.
29
+ */
30
+
31
+
32
+ import { AudioTrack } from "./tracks/AudioTrack.js";
33
+
34
+
35
+
36
+
37
+
38
+ /**
39
+ * @typedef {object} AudioInventoryEntry
40
+ * @property {number} index - The flat number everything downstream uses.
41
+ * @property {number} fileIndex - The torrent file this track lives in.
42
+ * @property {number} sourceTrackIndex - `0:a:N` WITHIN that file.
43
+ * @property {"embedded" | "sidecar"} kind - Whether it is muxed into the picture
44
+ * or ships as a file beside it. Not a type of track — a statement about where
45
+ * the bytes are.
46
+ * @property {string} codec
47
+ * @property {string} language - As the container states it, or "" when it does
48
+ * not. Never guessed here: what a folder name suggests is derived in the
49
+ * browser, where the language table and the viewer's locale already live.
50
+ * @property {string} languageBcp47
51
+ * @property {string} title
52
+ * @property {boolean} isDefault
53
+ * @property {boolean} declaresDefault
54
+ * @property {boolean} isOriginal
55
+ * @property {boolean} isCommentary
56
+ * @property {boolean} isVisualImpaired
57
+ * @property {boolean} isEnabled
58
+ * @property {number | null} channels
59
+ * @property {number | null} bitrateKbps - The rate the file states for this track; an average, not a bound.
60
+ * @property {string} fileName - For a sidecar: its own file name. "" otherwise.
61
+ * @property {string[]} folders - For a sidecar: the folders above it, relative
62
+ * to the torrent root. What the browser reads a language and a releaser from.
63
+ */
64
+
65
+ /**
66
+ * One numbered list from the picture's own tracks and its sidecar files.
67
+ *
68
+ * Order is load-bearing: embedded tracks keep the numbers they have always had,
69
+ * so a session created before this existed and one created after agree about
70
+ * what `audioTrackIndex: 1` means, and sidecars are appended after them in
71
+ * torrent-file order. Sidecar files are stable in that order for a given
72
+ * torrent, so the numbering is stable for a given file.
73
+ *
74
+ * @param {object} params
75
+ * @param {object[]} params.embedded - Merged banner+container tracks of the video.
76
+ * @param {number} params.videoFileIndex
77
+ * @param {Array<{ file: import("../torrent/files.js").SidecarFile, tracks: object[] }>} params.sidecars
78
+ * Each sidecar file with the audio tracks IT holds. A file whose container
79
+ * could not be read contributes one track, which is what a bare elementary
80
+ * stream is.
81
+ * @returns {AudioInventoryEntry[]}
82
+ */
83
+ export function buildAudioInventory({ embedded, videoFileIndex, sidecars }) {
84
+ /** @type {AudioInventoryEntry[]} */
85
+ const inventory = [];
86
+ const add = (track, fileIndex, sourceTrackIndex, kind, file) => {
87
+ inventory.push({
88
+ index: inventory.length,
89
+ fileIndex,
90
+ sourceTrackIndex,
91
+ kind,
92
+ // The name the browser judges "can I play this?" by. For an embedded
93
+ // track it is ffmpeg's own; for a sidecar it is translated from what the
94
+ // container wrote, or from the extension when there was no table to read.
95
+ codec: AudioTrack.codecNameOf(track, file?.extension ?? ""),
96
+ language: typeof track?.language === "string" ? track.language : "",
97
+ languageBcp47: typeof track?.languageBcp47 === "string" ? track.languageBcp47 : "",
98
+ title:
99
+ typeof track?.title === "string" && track.title.length > 0
100
+ ? track.title
101
+ : (typeof track?.name === "string" ? track.name : ""),
102
+ isDefault: track?.isDefault === true,
103
+ declaresDefault: track?.declaresDefault === true,
104
+ isOriginal: track?.isOriginal === true,
105
+ isCommentary: track?.isCommentary === true,
106
+ isVisualImpaired: track?.isVisualImpaired === true,
107
+ isEnabled: track?.isEnabled !== false,
108
+ channels: Number.isFinite(track?.channels) ? track.channels : null,
109
+ // The rate the file STATES for this track, in kbit/s — an average, not a
110
+ // bound — or null where nothing states one (a sidecar read from its
111
+ // container table, a Matroska stream without statistics tags). What a
112
+ // viewer's link is asked to carry for a copied soundtrack is this.
113
+ bitrateKbps: Number.isFinite(track?.bitrateKbps) && track.bitrateKbps > 0 ? track.bitrateKbps : null,
114
+ fileName: kind === "sidecar" ? (file?.name ?? "") : "",
115
+ folders: kind === "sidecar" && Array.isArray(file?.folders) ? file.folders : [],
116
+ // What the file's own path says, for a track that ships as its own file:
117
+ // read here, by the grammar that also decided this file belongs to this
118
+ // picture. The browser used to read the same path again to build the
119
+ // label, which is one name read twice by two rules.
120
+ naming: kind === "sidecar" ? (file?.naming ?? null) : null
121
+ });
122
+ };
123
+
124
+ for (const [order, track] of (Array.isArray(embedded) ? embedded : []).entries()) {
125
+ add(track, videoFileIndex, order, "embedded", null);
126
+ }
127
+ for (const sidecar of Array.isArray(sidecars) ? sidecars : []) {
128
+ const tracks = Array.isArray(sidecar?.tracks) && sidecar.tracks.length > 0
129
+ ? sidecar.tracks
130
+ // A file whose track table could not be read is one track: `.ac3`, `.dts`
131
+ // and `.mp3` have no table to read, and a `.mka` whose head has not
132
+ // arrived yet is better offered than hidden — ffmpeg will find its first
133
+ // audio stream either way.
134
+ : [{}];
135
+ for (const [order, track] of tracks.entries()) {
136
+ add(track, sidecar.file.fileIndex, order, "sidecar", sidecar.file);
137
+ }
138
+ }
139
+ return inventory;
140
+ }
141
+
142
+ /**
143
+ * Resolve the flat number back to the file and the track inside it.
144
+ *
145
+ * @param {AudioInventoryEntry[]} inventory
146
+ * @param {number} index
147
+ * @returns {AudioInventoryEntry | null}
148
+ */
149
+ export function resolveAudioIndex(inventory, index) {
150
+ if (!Array.isArray(inventory) || !Number.isInteger(index) || index < 0) {
151
+ return null;
152
+ }
153
+ return inventory.find((entry) => entry.index === index) ?? null;
154
+ }
155
+
156
+ /**
157
+ * Enrich the already-published first entry for a sidecar without changing the
158
+ * inventory's length or any address used by a player.
159
+ *
160
+ * A late container read can reveal language, codec and flags after the HLS
161
+ * master playlist has been loaded. Adding entries here would make the browser's
162
+ * rendition order disagree with the playlist it already holds.
163
+ *
164
+ * @param {AudioInventoryEntry[]} inventory
165
+ * @param {import("../torrent/files.js").SidecarFile} file
166
+ * @param {object} declaredTrack
167
+ * @returns {boolean} Whether an existing entry was updated.
168
+ */
169
+ export function enrichAudioInventoryFromSidecar(inventory, file, declaredTrack) {
170
+ if (!Array.isArray(inventory) || !file || !declaredTrack) return false;
171
+ const current = inventory.find(
172
+ (entry) => entry.kind === "sidecar" && entry.fileIndex === file.fileIndex && entry.sourceTrackIndex === 0
173
+ );
174
+ if (!current) return false;
175
+ const enriched = buildAudioInventory({
176
+ embedded: [],
177
+ videoFileIndex: -1,
178
+ sidecars: [{ file, tracks: [declaredTrack] }]
179
+ })[0];
180
+ Object.assign(current, enriched, {
181
+ index: current.index,
182
+ fileIndex: current.fileIndex,
183
+ sourceTrackIndex: current.sourceTrackIndex,
184
+ kind: current.kind
185
+ });
186
+ return true;
187
+ }
188
+
189
+ /**
190
+ * The name an `#EXT-X-MEDIA` line carries for one soundtrack.
191
+ *
192
+ * Deliberately plain, and deliberately NOT localised: this is the name inside a
193
+ * playlist, and what the viewer reads in the menu is composed in the browser
194
+ * from the same facts, where the language table and the viewer's own locale are.
195
+ * The only requirements here are that it says something and that no two
196
+ * renditions of one file share it — hls.js groups renditions by name.
197
+ *
198
+ * @param {AudioInventoryEntry} entry
199
+ * @param {AudioInventoryEntry[]} inventory
200
+ * @returns {string}
201
+ */
202
+ export function audioRenditionName(entry, inventory) {
203
+ const parts = [];
204
+ if (entry.title) {
205
+ parts.push(entry.title);
206
+ } else if (entry.languageBcp47 || entry.language) {
207
+ parts.push(entry.languageBcp47 || entry.language);
208
+ } else if (entry.folders.length > 0) {
209
+ // The folder a dub sits in is usually the only thing naming it, and a
210
+ // release names it for a reason: "Rus Sound", "Ukr Dub".
211
+ parts.push(entry.folders[entry.folders.length - 1]);
212
+ } else if (entry.fileName) {
213
+ parts.push(entry.fileName);
214
+ } else {
215
+ parts.push(`Track ${entry.index + 1}`);
216
+ }
217
+ if (entry.isCommentary) {
218
+ parts.push("commentary");
219
+ } else if (entry.isVisualImpaired) {
220
+ parts.push("described");
221
+ }
222
+ const name = parts.join(" · ");
223
+ const clash = (Array.isArray(inventory) ? inventory : []).some(
224
+ (other) => other.index !== entry.index && audioRenditionNameCore(other) === audioRenditionNameCore(entry)
225
+ );
226
+ return clash ? `${name} (${entry.index + 1})` : name;
227
+ }
228
+
229
+ /**
230
+ * The part of a rendition name that a clash is judged on — the name without the
231
+ * disambiguating number, so that adding the number cannot itself cause a clash.
232
+ *
233
+ * @param {AudioInventoryEntry} entry
234
+ * @returns {string}
235
+ */
236
+ function audioRenditionNameCore(entry) {
237
+ return (
238
+ entry.title ||
239
+ entry.languageBcp47 ||
240
+ entry.language ||
241
+ (entry.folders.length > 0 ? entry.folders[entry.folders.length - 1] : "") ||
242
+ entry.fileName ||
243
+ ""
244
+ );
245
+ }