@torrent-tv/proxy 2.80.19 → 2.81.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/research/double-spawn-2026-09-10.md +171 -0
  4. package/services/disk/DiskSpace.js +150 -0
  5. package/services/disk/wire.js +60 -0
  6. package/services/encode/EncodeRun.js +25 -3
  7. package/services/encode/SegmentStore.js +137 -9
  8. package/services/hls-session-manager.js +26 -45
  9. package/services/orchestrators/EncodeOrchestrator.js +4 -1
  10. package/services/piece-store/allowance.js +107 -0
  11. package/services/piece-store/piece-disk-store.js +365 -0
  12. package/services/piece-store/shared-piece-store.js +1549 -1535
  13. package/services/torrent-worker/client.js +32 -0
  14. package/services/torrent-worker/pool-adapter.js +15 -0
  15. package/services/torrent-worker/protocol.js +9 -0
  16. package/services/torrent-worker/worker.js +8 -1
  17. package/services/viewer/positions.js +48 -0
  18. package/test/audio-inventory.test.js +176 -176
  19. package/test/auto-quality-step.test.js +514 -514
  20. package/test/concurrent-cost.test.js +138 -138
  21. package/test/coverage-follows-the-disk.test.js +191 -191
  22. package/test/coverage-map.test.js +195 -195
  23. package/test/declared-tracks.test.js +35 -35
  24. package/test/disk-space.test.js +150 -0
  25. package/test/encode-orchestrator.test.js +0 -3
  26. package/test/encode-run.test.js +5 -12
  27. package/test/held-request-width.test.js +155 -155
  28. package/test/helpers/encode-run.js +2 -2
  29. package/test/matroska-blocks.test.js +0 -0
  30. package/test/matroska-cues-track.test.js +192 -192
  31. package/test/mp4-composition-times.test.js +0 -0
  32. package/test/mp4-subtitles.test.js +173 -173
  33. package/test/one-authority.test.js +281 -220
  34. package/test/orchestrator-wired.test.js +199 -199
  35. package/test/packet-witness-ring.test.js +236 -236
  36. package/test/packet-witness.test.js +148 -148
  37. package/test/piece-disk-store.test.js +267 -0
  38. package/test/piece-reader.test.js +4 -4
  39. package/test/piece-store-eviction.test.js +17 -17
  40. package/test/piece-store-reservations.test.js +20 -1
  41. package/test/piece-store-slow-disk.test.js +16 -1
  42. package/test/read-window.test.js +6 -6
  43. package/test/run-intervals.test.js +100 -100
  44. package/test/seek-landing.test.js +109 -109
  45. package/test/segment-store-eviction.test.js +232 -0
  46. package/test/segments-are-shared.test.js +1 -1
  47. package/test/shared-piece-store.test.js +12 -12
  48. package/test/sidecar-naming.test.js +142 -142
  49. package/test/subtitle-cue-framing.test.js +200 -200
  50. package/test/subtitle-cue-walk.test.js +369 -369
  51. package/test/subtitle-defaults.test.js +97 -97
  52. package/test/subtitle-track-numbering.test.js +370 -370
  53. package/test/tail-duplication.test.js +167 -167
  54. package/test/tracks-begin-together.test.js +195 -195
  55. package/test/two-viewers-one-picture.test.js +374 -374
  56. package/test/video-facts.test.js +102 -102
  57. package/test/wedge-certainty.test.js +131 -131
  58. package/services/piece-store/disk-tier.js +0 -151
@@ -676,6 +676,38 @@ export class TorrentWorkerClient {
676
676
  *
677
677
  * @returns {Promise<void>}
678
678
  */
679
+ /**
680
+ * What the spilled pieces weigh, as of the last revision. Zero until one has
681
+ * happened, which is what "we have not asked yet" means.
682
+ *
683
+ * @type {number}
684
+ */
685
+ spilledBytes = 0;
686
+
687
+ /**
688
+ * Say how much disk the spilled pieces on this thread may take between them.
689
+ *
690
+ * Best effort and never awaited by anything that matters: a share that does
691
+ * not arrive leaves the ceiling where it was, and the next revision is a
692
+ * minute away.
693
+ *
694
+ * @param {number} bytes
695
+ * @returns {Promise<void>}
696
+ */
697
+ async allowSpillBytes(bytes) {
698
+ try {
699
+ const revised = await this.#caller.call(Command.SPILL_ALLOWANCE, { bytes });
700
+ // The reply says what those stores actually hold, which is what the owner
701
+ // of the disk needs for the next division. One exchange, both directions.
702
+ this.spilledBytes = Array.isArray(revised)
703
+ ? revised.reduce((sum, store) => sum + (Number(store?.bytes) || 0), 0)
704
+ : this.spilledBytes;
705
+ } catch {
706
+ // The thread is gone or busy; the next revision says it again.
707
+ }
708
+ return this.spilledBytes;
709
+ }
710
+
679
711
  async destroyAll() {
680
712
  this.#stopping = true;
681
713
  try {
@@ -70,6 +70,21 @@ export class WorkerTorrentPool {
70
70
  * @param {number} fileIndex
71
71
  * @returns {() => void}
72
72
  */
73
+ /** What the spilled pieces weigh on the torrent thread, as last revised. */
74
+ get spilledBytes() {
75
+ return this.#client.spilledBytes;
76
+ }
77
+
78
+ /**
79
+ * Say how much disk those spilled pieces may take between them.
80
+ *
81
+ * @param {number} bytes
82
+ * @returns {Promise<number>} What they hold now.
83
+ */
84
+ allowSpillBytes(bytes) {
85
+ return this.#client.allowSpillBytes(bytes);
86
+ }
87
+
73
88
  acquireFile(torrent, fileIndex) {
74
89
  const sourceKey = torrent?.sourceKey;
75
90
  if (!sourceKey) {
@@ -115,6 +115,15 @@ export const Command = {
115
115
  /** Cues of one subtitle track, from the clusters already downloaded. */
116
116
  SUBTITLE_CUES: "subtitle-cues",
117
117
  /** Shut the client down, optionally deleting downloaded data. */
118
+ /**
119
+ * How much disk the spilled pieces may take between them.
120
+ *
121
+ * Sent from the main thread, because the disk has one owner and this store is
122
+ * not its only user: the segments an encoder produces are on the same disk,
123
+ * and a ceiling one of two users sets for itself is not a ceiling. The worker
124
+ * divides its share between the stores it holds.
125
+ */
126
+ SPILL_ALLOWANCE: "spill-allowance",
118
127
  DESTROY_ALL: "destroy-all"
119
128
  };
120
129
 
@@ -65,7 +65,7 @@ forwardLogsTo((_level, message) => {
65
65
  // the hook above had a chance to register. Verified the hard way: with a static
66
66
  // import the process still aborted, and the stack named the genuine polyfill.
67
67
  const { TorrentPool, resolveDhtBootstrap } = await import("../torrent-pool.js");
68
- const { collectStoreStats, machineReserveBytes, pieceBufferCollection, reviseStoreBudgets } =
68
+ const { collectStoreStats, machineReserveBytes, pieceBufferCollection, reviseSpillBudgets, reviseStoreBudgets } =
69
69
  await import("../piece-store/shared-piece-store.js");
70
70
 
71
71
  // Resolved before the client exists, because the client builds its DHT in its
@@ -580,6 +580,13 @@ async function runCommand(command, params, id) {
580
580
  return true;
581
581
  }
582
582
 
583
+ case Command.SPILL_ALLOWANCE: {
584
+ // The disk has one owner and it is on the main thread, where the segments
585
+ // are. What arrives is this thread’s whole share; the stores divide it
586
+ // between themselves.
587
+ return reviseSpillBudgets(Number(params.bytes));
588
+ }
589
+
583
590
  case Command.DESTROY_ALL: {
584
591
  fileClaims.closeAll();
585
592
  torrentsByKey.clear();
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @file Where the viewers of one output stand.
3
+ *
4
+ * Asked by whatever has to give disk back: what lies behind every viewer has
5
+ * been played and will not be wanted again unless somebody seeks back, and what
6
+ * lies ahead of the furthest will be wanted eventually. So an answer of "nobody"
7
+ * is the strongest statement there is — everything that output holds is worth
8
+ * less than anything anybody is on their way to.
9
+ *
10
+ * Pure: it is handed the sessions, the clock and a way to turn seconds into a
11
+ * segment number, and it holds none of them.
12
+ */
13
+
14
+ import { viewersOf } from "./Viewer.js";
15
+
16
+ /**
17
+ * @param {object} params
18
+ * @param {Iterable<{ outputKey?: string }>} params.sessions - Every live session.
19
+ * @param {string} params.outputKey - The output being asked about.
20
+ * @param {(session: object, seconds: number) => number} params.segmentAt - Which
21
+ * segment of THAT session's timeline a moment of film falls in.
22
+ * @param {number} params.now
23
+ * @param {number} params.staleAfterMs - Silence longer than any a watching
24
+ * viewer can produce.
25
+ * @returns {number[]} A segment number per present viewer, unsorted.
26
+ */
27
+ export function viewerSegmentsOn({ sessions, outputKey, segmentAt, now, staleAfterMs }) {
28
+ const at = [];
29
+ for (const session of sessions) {
30
+ if (session.outputKey !== outputKey) {
31
+ continue;
32
+ }
33
+ for (const viewer of viewersOf(session).values()) {
34
+ if (!viewer.isPresent(now, staleAfterMs)) {
35
+ continue;
36
+ }
37
+ const seconds = viewer.positionSeconds();
38
+ if (!Number.isFinite(seconds)) {
39
+ continue;
40
+ }
41
+ const index = segmentAt(session, /** @type {number} */ (seconds));
42
+ if (Number.isInteger(index) && index >= 0) {
43
+ at.push(index);
44
+ }
45
+ }
46
+ }
47
+ return at;
48
+ }
@@ -1,176 +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 { 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
- });
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
+ });