@torrent-tv/proxy 2.74.1 → 2.75.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.
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +8 -5
- package/docs/container-architecture.md +25 -5
- package/package.json +1 -1
- package/services/audio-inventory.js +201 -411
- package/services/container/AviContainer.js +266 -81
- package/services/container/Container.js +248 -79
- package/services/container/ContainerFactory.js +67 -0
- package/services/container/MatroskaContainer.js +326 -7
- package/services/container/Mp4Container.js +372 -26
- package/services/hls-session-manager.js +2 -2
- package/services/playback-planner.js +43 -15
- package/services/torrent-worker/pool-adapter.js +352 -333
- package/services/tracks/AudioTrack.js +131 -40
- package/test/audio-inventory.test.js +176 -177
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/video-facts.test.js +102 -0
- package/services/container-index/avi.js +0 -167
- package/services/container-index/index.js +0 -118
- package/services/container-index/matroska.js +0 -336
- package/services/container-index/mp4.js +0 -358
- /package/services/{container-index → container}/ebml-reader.js +0 -0
|
@@ -1,40 +1,131 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Audio track — extends ContainerTrack with audio-specific flags.
|
|
3
|
-
*
|
|
4
|
-
* Matroska RFC 9559 §5.1.4.1:
|
|
5
|
-
* - FlagOriginal 0x55AE (default 0) — track is original language.
|
|
6
|
-
* - FlagCommentary 0x55AF — commentary track.
|
|
7
|
-
* - FlagVisualImpaired 0x55AC — audio description for visually impaired.
|
|
8
|
-
* - FlagTextDescriptions 0x55AD, FlagHearingImpaired etc. may also appear
|
|
9
|
-
* but the three above drive the audio menu (research/container-spec-conformance).
|
|
10
|
-
* MP4 ISO/IEC 14496-12: tkhd alternate_group groups alternate audio tracks.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { ContainerTrack } from "./ContainerTrack.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Audio track — extends ContainerTrack with audio-specific flags.
|
|
3
|
+
*
|
|
4
|
+
* Matroska RFC 9559 §5.1.4.1:
|
|
5
|
+
* - FlagOriginal 0x55AE (default 0) — track is original language.
|
|
6
|
+
* - FlagCommentary 0x55AF — commentary track.
|
|
7
|
+
* - FlagVisualImpaired 0x55AC — audio description for visually impaired.
|
|
8
|
+
* - FlagTextDescriptions 0x55AD, FlagHearingImpaired etc. may also appear
|
|
9
|
+
* but the three above drive the audio menu (research/container-spec-conformance).
|
|
10
|
+
* MP4 ISO/IEC 14496-12: tkhd alternate_group groups alternate audio tracks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ContainerTrack } from "./ContainerTrack.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Codec identifiers as containers write them, against the name ffmpeg prints.
|
|
17
|
+
*
|
|
18
|
+
* Needed because the browser decides whether it can play a soundtrack from that
|
|
19
|
+
* name, and for a sidecar file there is no ffmpeg banner to read it from — the
|
|
20
|
+
* track came from the container's own table, where Matroska writes `A_AC3` and
|
|
21
|
+
* MP4 writes `ac-3` for the thing ffmpeg calls `ac3`. Only what a soundtrack can
|
|
22
|
+
* actually be is listed; an identifier not here is reported as it was written,
|
|
23
|
+
* which the browser treats as one it does not know and therefore transcodes.
|
|
24
|
+
*/
|
|
25
|
+
const CODEC_NAMES = new Map([
|
|
26
|
+
["A_AAC", "aac"],
|
|
27
|
+
["A_AC3", "ac3"],
|
|
28
|
+
["A_EAC3", "eac3"],
|
|
29
|
+
["A_DTS", "dts"],
|
|
30
|
+
["A_FLAC", "flac"],
|
|
31
|
+
["A_OPUS", "opus"],
|
|
32
|
+
["A_VORBIS", "vorbis"],
|
|
33
|
+
["A_TRUEHD", "truehd"],
|
|
34
|
+
["A_MPEG/L3", "mp3"],
|
|
35
|
+
["A_MPEG/L2", "mp2"],
|
|
36
|
+
["A_ALAC", "alac"],
|
|
37
|
+
["mp4a", "aac"],
|
|
38
|
+
["ac-3", "ac3"],
|
|
39
|
+
["ec-3", "eac3"],
|
|
40
|
+
["alac", "alac"],
|
|
41
|
+
["opus", "opus"],
|
|
42
|
+
["Opus", "opus"],
|
|
43
|
+
["fLaC", "flac"],
|
|
44
|
+
["flac", "flac"]
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extensions of raw elementary streams, against the codec they carry.
|
|
49
|
+
*
|
|
50
|
+
* A bare `.ac3` has no track table to read, and its extension is the only thing
|
|
51
|
+
* that states its codec — which for an elementary stream is exactly what the
|
|
52
|
+
* extension means.
|
|
53
|
+
*/
|
|
54
|
+
const CODEC_BY_EXTENSION = new Map([
|
|
55
|
+
[".aac", "aac"],
|
|
56
|
+
[".ac3", "ac3"],
|
|
57
|
+
[".eac3", "eac3"],
|
|
58
|
+
[".dts", "dts"],
|
|
59
|
+
[".dtshd", "dts"],
|
|
60
|
+
[".flac", "flac"],
|
|
61
|
+
[".mp3", "mp3"],
|
|
62
|
+
[".mp2", "mp2"],
|
|
63
|
+
[".opus", "opus"],
|
|
64
|
+
[".ogg", "vorbis"],
|
|
65
|
+
[".oga", "vorbis"],
|
|
66
|
+
[".wav", "pcm"],
|
|
67
|
+
[".thd", "truehd"],
|
|
68
|
+
[".mlp", "truehd"],
|
|
69
|
+
[".m4a", "aac"]
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
export class AudioTrack extends ContainerTrack {
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} params - See ContainerTrack plus:
|
|
75
|
+
* @param {boolean} params.isOriginal - FlagOriginal
|
|
76
|
+
* @param {boolean} params.isCommentary - FlagCommentary
|
|
77
|
+
* @param {boolean} params.isVisualImpaired - FlagVisualImpaired / descriptive audio
|
|
78
|
+
* @param {number | null} params.channels
|
|
79
|
+
* @param {number | null} params.samplingFrequency
|
|
80
|
+
*/
|
|
81
|
+
constructor(params) {
|
|
82
|
+
super({ ...params, type: "audio" });
|
|
83
|
+
this.isOriginal = params.isOriginal === true;
|
|
84
|
+
this.isCommentary = params.isCommentary === true;
|
|
85
|
+
this.isVisualImpaired = params.isVisualImpaired === true;
|
|
86
|
+
this.channels = Number.isFinite(params.channels) ? params.channels : null;
|
|
87
|
+
this.samplingFrequency = Number.isFinite(params.samplingFrequency) ? params.samplingFrequency : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The ffmpeg-side codec name for one track, from whatever the reading gave.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ codec?: string, codecId?: string }} track
|
|
94
|
+
* @param {string} [extension] - The sidecar file's extension, when the track
|
|
95
|
+
* came from a file with no readable table.
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
static codecNameOf(track, extension = "") {
|
|
99
|
+
const fromBanner = typeof track?.codec === "string" ? track.codec.trim() : "";
|
|
100
|
+
if (fromBanner.length > 0) {
|
|
101
|
+
return fromBanner.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
const codecId = typeof track?.codecId === "string" ? track.codecId.trim() : "";
|
|
104
|
+
if (codecId.length > 0) {
|
|
105
|
+
const known = CODEC_NAMES.get(codecId);
|
|
106
|
+
if (known) {
|
|
107
|
+
return known;
|
|
108
|
+
}
|
|
109
|
+
// Matroska allows a suffix — `A_AAC/MPEG4/LC`, `A_PCM/INT/LIT` — so the
|
|
110
|
+
// family is what the first two segments say.
|
|
111
|
+
const family = codecId.split("/").slice(0, 2).join("/");
|
|
112
|
+
const byFamily = CODEC_NAMES.get(family) ?? CODEC_NAMES.get(codecId.split("/")[0]);
|
|
113
|
+
if (byFamily) {
|
|
114
|
+
return byFamily;
|
|
115
|
+
}
|
|
116
|
+
if (codecId.startsWith("A_PCM")) {
|
|
117
|
+
return "pcm";
|
|
118
|
+
}
|
|
119
|
+
return codecId.toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
return CODEC_BY_EXTENSION.get(extension) ?? "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Human label helper — commentary and descriptive audio must not look like main track. */
|
|
125
|
+
audioRoleLabel() {
|
|
126
|
+
if (this.isCommentary) return "commentary";
|
|
127
|
+
if (this.isVisualImpaired) return "descriptive";
|
|
128
|
+
if (this.isOriginal) return "original";
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,177 +1,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
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ index:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ language: "eng", name: "",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
assert.equal(merged.
|
|
52
|
-
assert.equal(merged.tracks[0].
|
|
53
|
-
assert.equal(merged.tracks[
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[{
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
assert.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
assert.equal(merged.tracks[0].
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[{
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
assert.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{ language: "
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assert.deepEqual(inventory.map((entry) => entry.
|
|
97
|
-
assert.deepEqual(inventory.map((entry) => entry.
|
|
98
|
-
assert.deepEqual(inventory.map((entry) => entry.
|
|
99
|
-
assert.
|
|
100
|
-
assert.
|
|
101
|
-
assert.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
assert.equal(inventory.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
assert.equal(resolveAudioIndex(inventory, 1).
|
|
138
|
-
assert.equal(resolveAudioIndex(inventory,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
assert.equal(codecNameOf({
|
|
144
|
-
assert.equal(codecNameOf({ codecId: "
|
|
145
|
-
assert.equal(codecNameOf({ codecId: "
|
|
146
|
-
assert.equal(codecNameOf({ codecId: "
|
|
147
|
-
assert.equal(codecNameOf({
|
|
148
|
-
assert.equal(codecNameOf({}, ".
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{ file: { fileIndex:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
audioRenditionName(inventory[
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
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/container/Container.js";
|
|
19
|
+
import { AudioTrack } from "../services/tracks/AudioTrack.js";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import {
|
|
22
|
+
audioRenditionName,
|
|
23
|
+
buildAudioInventory,
|
|
24
|
+
resolveAudioIndex
|
|
25
|
+
} from "../services/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
|
+
});
|