@torrent-tv/proxy 2.88.3 → 2.88.5
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 +16 -0
- package/assets/calibration/audio/NOTICE.md +19 -0
- package/assets/calibration/audio/aac.m4a +0 -0
- package/assets/calibration/audio/ac3.ac3 +0 -0
- package/assets/calibration/audio/alac.m4a +0 -0
- package/assets/calibration/audio/dts.dts +0 -0
- package/assets/calibration/audio/eac3.eac3 +0 -0
- package/assets/calibration/audio/flac.flac +0 -0
- package/assets/calibration/audio/manifest.json +98 -0
- package/assets/calibration/audio/mp2.mp2 +0 -0
- package/assets/calibration/audio/mp3.mp3 +0 -0
- package/assets/calibration/audio/opus.opus +0 -0
- package/assets/calibration/audio/pcm.wav +0 -0
- package/assets/calibration/audio/truehd.thd +0 -0
- package/assets/calibration/audio/vorbis.ogg +0 -0
- package/biome.json +1 -0
- package/docs/browser-proxy-contract.md +155 -133
- package/docs/playback-start-forecast.md +56 -0
- package/package.json +1 -1
- package/routes/api/transcode-sessions/post.js +5 -0
- package/routes/transcode/audio-warm/get.js +14 -3
- package/scripts/build-audio-samples.mjs +31 -0
- package/server.js +5 -0
- package/services/encode/EncodeRuns.js +3 -1
- package/services/encode/OutputOpening.js +40 -15
- package/services/encode/Renditions.js +129 -31
- package/services/encode/args.js +5 -3
- package/services/encode/audio-calibration.js +67 -0
- package/services/encode/audio-work-rate.js +15 -0
- package/services/encode/hwaccel.js +1 -1
- package/services/encode/quality/EncodeCost.js +29 -3
- package/services/encode/quality/link-budget.js +110 -15
- package/services/encode/quality/output-format.js +10 -6
- package/services/encode/segment-formats/fmp4.js +2 -0
- package/services/encode/segment-formats/mp4-boxes.js +12 -1
- package/services/media/audio-inventory.js +60 -6
- package/services/media/container/Container.js +7 -1
- package/services/media/container/Mp4Container.js +103 -3
- package/services/media/tracks/AudioTrack.js +12 -0
- package/services/media/tracks/audio-bound.js +233 -0
- package/services/server/ViewerRequests.js +30 -5
- package/services/server/playback-readiness.js +516 -708
- package/services/server/wire-outputs.js +31 -3
- package/services/storage/piece-store/shared-piece-store.js +6 -0
- package/services/storage/segment-store/SegmentStore.js +19 -4
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/torrent-pool.js +13 -0
- package/test/audio-buffer-continuity.test.js +70 -70
- package/test/audio-calibration.test.js +23 -0
- package/test/episode-naming.test.js +131 -131
- package/test/fixtures/audio-frame-boundary.json +1 -1
- package/test/fmp4-presentation-clock.test.js +17 -2
- package/test/link-budget.test.js +7 -6
- package/test/one-output-one-session.test.js +77 -8
- package/test/playback-readiness.test.js +36 -6
- package/test/quality-variants.test.js +36 -8
- package/test/segment-store.test.js +15 -0
- package/test/serving-existing-output.test.js +3 -0
- package/test/soundtrack-rate-bound.test.js +289 -0
- package/test/source-files-route.test.js +113 -113
- package/test/two-viewers-one-picture.test.js +97 -0
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import test from "node:test";
|
|
4
|
-
import { readPresentationRanges, translatePresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
|
-
import { predictPlaybackReadiness } from "../services/server/playback-readiness.js";
|
|
6
|
-
|
|
7
|
-
const evidence = JSON.parse(readFileSync(new URL("./fixtures/audio-frame-boundary.json", import.meta.url), "utf8"));
|
|
8
|
-
const header = Buffer.from(evidence.files["init.mp4"], "base64");
|
|
9
|
-
|
|
10
|
-
function ranges(name, shiftedFrames = 0) {
|
|
11
|
-
const bytes = Buffer.from(evidence.files[name], "base64");
|
|
12
|
-
let frameTicks;
|
|
13
|
-
walkBoxes(bytes, (type, start) => {
|
|
14
|
-
if (type === "tfhd") {
|
|
15
|
-
const flags = bytes.readUIntBE(start + 1, 3);
|
|
16
|
-
const at = start + 8 + ((flags & 1) ? 8 : 0) + ((flags & 2) ? 4 : 0);
|
|
17
|
-
if (flags & 8) frameTicks = bytes.readUInt32BE(at);
|
|
18
|
-
}
|
|
19
|
-
if (type === "tfdt" && shiftedFrames) {
|
|
20
|
-
assert.ok(frameTicks > 0);
|
|
21
|
-
bytes.writeBigUInt64BE(bytes.readBigUInt64BE(start + 4) + BigInt(frameTicks * shiftedFrames), start + 4);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
return translatePresentationRanges(readPresentationRanges(Buffer.concat([header, bytes])), header, 0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function state(second = ranges("segment-00172.mp4")) {
|
|
28
|
-
const first = ranges("segment-00171.mp4");
|
|
29
|
-
const now = 1000;
|
|
30
|
-
return {
|
|
31
|
-
now,
|
|
32
|
-
positionSeconds: first[0].start,
|
|
33
|
-
durationSeconds: second.at(-1).end,
|
|
34
|
-
bufferLimitSeconds: second.at(-1).end - first[0].start,
|
|
35
|
-
reserveSeconds: 0,
|
|
36
|
-
lookaheadSeconds: second.at(-1).end - first[0].start,
|
|
37
|
-
sources: [{ id: "source", complete: true }],
|
|
38
|
-
linkReadings: [{ at: now, value: 1_000_000 }],
|
|
39
|
-
tracks: [{
|
|
40
|
-
id: "audio", sourceIds: ["source"], clientRanges: [
|
|
41
|
-
{ start: evidence.browserBufferedRanges[0][0], end: evidence.browserBufferedRanges[0][1] }
|
|
42
|
-
],
|
|
43
|
-
readings: [{ at: now, value: 1 }],
|
|
44
|
-
readySegmentIndices: [0, 1], segmentSizesBytes: { 0: 170000, 1: 170000 },
|
|
45
|
-
segments: [
|
|
46
|
-
{ index: 0, startSeconds: first[0].start, endSeconds: second[0].start, mediaRanges: first },
|
|
47
|
-
{ index: 1, startSeconds: second[0].start, endSeconds: second.at(-1).end, mediaRanges: second }
|
|
48
|
-
]
|
|
49
|
-
}]
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
test("captured AAC boundary agrees with Chrome's continuous buffered range", () => {
|
|
54
|
-
const input = state();
|
|
55
|
-
assert.equal(predictPlaybackReadiness(input).ready, true);
|
|
56
|
-
for (const segment of input.tracks[0].segments) {
|
|
57
|
-
segment.mediaRanges = segment.mediaRanges.map(({ start, end }) => ({ start, end }));
|
|
58
|
-
}
|
|
59
|
-
assert.equal(predictPlaybackReadiness(input).reason, "no-safe-start-found");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("four missing AAC frames preserve the discontinuity observed in Chrome", () => {
|
|
63
|
-
assert.equal(predictPlaybackReadiness(state(ranges("segment-00172.mp4", 4))).reason, "no-safe-start-found");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("a join boundary never supplies audio before the adjacent segment arrives", () => {
|
|
67
|
-
const input = state();
|
|
68
|
-
input.linkReadings[0].value = 1;
|
|
69
|
-
assert.notEqual(predictPlaybackReadiness(input).ready, true);
|
|
70
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readPresentationRanges, translatePresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
|
+
import { predictPlaybackReadiness } from "../services/server/playback-readiness.js";
|
|
6
|
+
|
|
7
|
+
const evidence = JSON.parse(readFileSync(new URL("./fixtures/audio-frame-boundary.json", import.meta.url), "utf8"));
|
|
8
|
+
const header = Buffer.from(evidence.files["init.mp4"], "base64");
|
|
9
|
+
|
|
10
|
+
function ranges(name, shiftedFrames = 0) {
|
|
11
|
+
const bytes = Buffer.from(evidence.files[name], "base64");
|
|
12
|
+
let frameTicks;
|
|
13
|
+
walkBoxes(bytes, (type, start) => {
|
|
14
|
+
if (type === "tfhd") {
|
|
15
|
+
const flags = bytes.readUIntBE(start + 1, 3);
|
|
16
|
+
const at = start + 8 + ((flags & 1) ? 8 : 0) + ((flags & 2) ? 4 : 0);
|
|
17
|
+
if (flags & 8) frameTicks = bytes.readUInt32BE(at);
|
|
18
|
+
}
|
|
19
|
+
if (type === "tfdt" && shiftedFrames) {
|
|
20
|
+
assert.ok(frameTicks > 0);
|
|
21
|
+
bytes.writeBigUInt64BE(bytes.readBigUInt64BE(start + 4) + BigInt(frameTicks * shiftedFrames), start + 4);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return translatePresentationRanges(readPresentationRanges(Buffer.concat([header, bytes])), header, 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function state(second = ranges("segment-00172.mp4")) {
|
|
28
|
+
const first = ranges("segment-00171.mp4");
|
|
29
|
+
const now = 1000;
|
|
30
|
+
return {
|
|
31
|
+
now,
|
|
32
|
+
positionSeconds: first[0].start,
|
|
33
|
+
durationSeconds: second.at(-1).end,
|
|
34
|
+
bufferLimitSeconds: second.at(-1).end - first[0].start,
|
|
35
|
+
reserveSeconds: 0,
|
|
36
|
+
lookaheadSeconds: second.at(-1).end - first[0].start,
|
|
37
|
+
sources: [{ id: "source", complete: true }],
|
|
38
|
+
linkReadings: [{ at: now, value: 1_000_000 }],
|
|
39
|
+
tracks: [{
|
|
40
|
+
id: "audio", sourceIds: ["source"], clientRanges: [
|
|
41
|
+
{ start: evidence.browserBufferedRanges[0][0], end: evidence.browserBufferedRanges[0][1] }
|
|
42
|
+
],
|
|
43
|
+
readings: [{ at: now, value: 1 }],
|
|
44
|
+
readySegmentIndices: [0, 1], segmentSizesBytes: { 0: 170000, 1: 170000 },
|
|
45
|
+
segments: [
|
|
46
|
+
{ index: 0, startSeconds: first[0].start, endSeconds: second[0].start, mediaRanges: first },
|
|
47
|
+
{ index: 1, startSeconds: second[0].start, endSeconds: second.at(-1).end, mediaRanges: second }
|
|
48
|
+
]
|
|
49
|
+
}]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test("captured AAC boundary agrees with Chrome's continuous buffered range", () => {
|
|
54
|
+
const input = state();
|
|
55
|
+
assert.equal(predictPlaybackReadiness(input).ready, true);
|
|
56
|
+
for (const segment of input.tracks[0].segments) {
|
|
57
|
+
segment.mediaRanges = segment.mediaRanges.map(({ start, end }) => ({ start, end }));
|
|
58
|
+
}
|
|
59
|
+
assert.equal(predictPlaybackReadiness(input).reason, "no-safe-start-found");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("four missing AAC frames preserve the discontinuity observed in Chrome", () => {
|
|
63
|
+
assert.equal(predictPlaybackReadiness(state(ranges("segment-00172.mp4", 4))).reason, "no-safe-start-found");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a join boundary never supplies audio before the adjacent segment arrives", () => {
|
|
67
|
+
const input = state();
|
|
68
|
+
input.linkReadings[0].value = 1;
|
|
69
|
+
assert.notEqual(predictPlaybackReadiness(input).ready, true);
|
|
70
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { audioReadingFor } from "../services/encode/audio-calibration.js";
|
|
4
|
+
|
|
5
|
+
const readings = [
|
|
6
|
+
{ codec: "ac3", operation: "aac", channels: 2, samplingFrequency: 48000, speed: 120 },
|
|
7
|
+
{ codec: "aac", operation: "copy", channels: 2, samplingFrequency: 48000, speed: 200,
|
|
8
|
+
bytes: 64000, durationSeconds: 4 }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
test("audio processing uses codec and sample work instead of a video-copy rate", () => {
|
|
12
|
+
assert.equal(audioReadingFor(readings, { codec: "ac3", transcode: true, channels: 6,
|
|
13
|
+
samplingFrequency: 48000 }).speed, 40);
|
|
14
|
+
assert.equal(audioReadingFor(readings, { codec: "ac3", transcode: true, channels: 2,
|
|
15
|
+
samplingFrequency: 24000 }).speed, 240);
|
|
16
|
+
assert.equal(audioReadingFor(readings, { codec: "dts", transcode: true }), null);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("copying uses encoded byte work when the source declares its bitrate", () => {
|
|
20
|
+
assert.equal(audioReadingFor(readings, { codec: "aac", transcode: false, bitrateKbps: 256 }).speed, 100);
|
|
21
|
+
assert.equal(audioReadingFor(readings, { codec: "aac", transcode: false }).speed, 200);
|
|
22
|
+
assert.equal(audioReadingFor(readings, { codec: "aac", transcode: true }), null);
|
|
23
|
+
});
|
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a picture's name says about which episode it is, and what a whole
|
|
3
|
-
* torrent's pictures are taken together.
|
|
4
|
-
*
|
|
5
|
-
* Every name below is taken from a release in the survey collection, except
|
|
6
|
-
* where a test says it is built to reach an edge.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { readEpisodeMarker } from "../services/torrent/episode-naming.js";
|
|
12
|
-
import { TorrentContents } from "../services/torrent/Contents.js";
|
|
13
|
-
|
|
14
|
-
test("season and episode with the episode title after them", () => {
|
|
15
|
-
assert.deepEqual(readEpisodeMarker({ name: "s01e02_Murder.in.the.Mews.avi", folders: ["Season_01"] }), {
|
|
16
|
-
season: 1,
|
|
17
|
-
episodes: [2],
|
|
18
|
-
part: null,
|
|
19
|
-
special: false,
|
|
20
|
-
showHint: "",
|
|
21
|
-
titleHint: "Murder.in.the.Mews"
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("a separator between the season and the episode is still one marker", () => {
|
|
26
|
-
const marker = readEpisodeMarker({ name: "s03.e01_The.Mysterious.Affair.at.Styles.avi" });
|
|
27
|
-
assert.equal(marker.season, 3);
|
|
28
|
-
assert.deepEqual(marker.episodes, [1]);
|
|
29
|
-
assert.equal(marker.titleHint, "The.Mysterious.Affair.at.Styles");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("a part of one episode is its own field and leaves the title", () => {
|
|
33
|
-
const marker = readEpisodeMarker({ name: "s02e01_Peril.at.End.House_Part.1.avi" });
|
|
34
|
-
assert.equal(marker.part, 1);
|
|
35
|
-
assert.equal(marker.titleHint, "Peril.at.End.House");
|
|
36
|
-
assert.deepEqual(marker.episodes, [1]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("the show name comes before the marker and release tags after it", () => {
|
|
40
|
-
const marker = readEpisodeMarker({ name: "Reacher.S04E01.1080p.rus.LostFilm.TV.mkv" });
|
|
41
|
-
assert.equal(marker.season, 4);
|
|
42
|
-
assert.equal(marker.showHint, "Reacher");
|
|
43
|
-
assert.equal(marker.titleHint, "1080p.rus.LostFilm.TV");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("one file carrying several episodes lists all of them", () => {
|
|
47
|
-
assert.deepEqual(readEpisodeMarker({ name: "Show.S01E01E02.mkv" }).episodes, [1, 2]);
|
|
48
|
-
assert.deepEqual(readEpisodeMarker({ name: "Show.S01E01-E03.mkv" }).episodes, [1, 2, 3]);
|
|
49
|
-
assert.deepEqual(readEpisodeMarker({ name: "Show.S01E01-02.mkv" }).episodes, [1, 2]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("a claim of more episodes than a file carries is not a marker", () => {
|
|
53
|
-
assert.equal(readEpisodeMarker({ name: "Show.S01E01-40.mkv" }), null);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("season 0 and a name calling itself a special are specials", () => {
|
|
57
|
-
assert.equal(readEpisodeMarker({ name: "Show.S00E03.mkv" }).special, true);
|
|
58
|
-
assert.equal(readEpisodeMarker({ name: "Show - 05 Special.mkv" }).special, true);
|
|
59
|
-
assert.equal(readEpisodeMarker({ name: "Show.S01E03.mkv" }).special, false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("the numbering anime releases use, with no season in it", () => {
|
|
63
|
-
const marker = readEpisodeMarker({ name: "[HorribleSubs] Drifters - 01 [1080p].mkv" });
|
|
64
|
-
assert.equal(marker.season, null);
|
|
65
|
-
assert.deepEqual(marker.episodes, [1]);
|
|
66
|
-
assert.equal(marker.showHint, "[HorribleSubs] Drifters");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("a season stated just before the dash number is read", () => {
|
|
70
|
-
const marker = readEpisodeMarker({ name: "Koukaku Kidoutai (2026) S1 - 01.mkv" });
|
|
71
|
-
assert.equal(marker.season, 1);
|
|
72
|
-
assert.equal(marker.showHint, "Koukaku Kidoutai (2026)");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("a season folder gives the season to an episode number without one", () => {
|
|
76
|
-
const marker = readEpisodeMarker({ name: "Episode.05.mkv", folders: ["Сезон 2"] });
|
|
77
|
-
assert.equal(marker.season, 2);
|
|
78
|
-
assert.deepEqual(marker.episodes, [5]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("names that state no episode say nothing", () => {
|
|
82
|
-
for (const name of [
|
|
83
|
-
"Despicable.Me.4.2024.1080p.BluRay.x264-EniaHD.mkv",
|
|
84
|
-
"Movie - 2024 [1080p].mkv",
|
|
85
|
-
"WALL-E.2008.1080p.mkv",
|
|
86
|
-
"Mortal.Kombat.II.1080p.rus.LostFilm.TV.mkv",
|
|
87
|
-
"Video.1920x1080.mkv",
|
|
88
|
-
"VTS_01_1.VOB"
|
|
89
|
-
]) {
|
|
90
|
-
assert.equal(readEpisodeMarker({ name }), null, name);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @param {string} name
|
|
96
|
-
* @param {string[]} relatives
|
|
97
|
-
* @returns {TorrentContents}
|
|
98
|
-
*/
|
|
99
|
-
function contents(name, relatives) {
|
|
100
|
-
return new TorrentContents({
|
|
101
|
-
name,
|
|
102
|
-
files: relatives.map((relative) => ({
|
|
103
|
-
path: `${name}/${relative}`,
|
|
104
|
-
name: relative.slice(relative.lastIndexOf("/") + 1),
|
|
105
|
-
length: 1
|
|
106
|
-
}))
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
test("one picture is a single work", () => {
|
|
111
|
-
assert.equal(new TorrentContents({ name: "Film.mkv", files: [{ path: "Film.mkv", name: "Film.mkv", length: 1 }] }).shape, "single");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("numbered pictures of one show are a series, and an unnumbered one stays in it", () => {
|
|
115
|
-
const pack = contents("Poirot", ["Season_01/s01e01_A.avi", "Season_01/s01e02_B.avi", "Special.avi"]);
|
|
116
|
-
assert.equal(pack.shape, "series");
|
|
117
|
-
assert.equal(pack.items.find((item) => item.name === "Special.avi").episode, null);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("pictures without numbers are not called a collection, only not known", () => {
|
|
121
|
-
assert.equal(contents("Trilogy", ["One.2010.mkv", "Two.2013.mkv", "Three.2017.mkv"]).shape, "undetermined");
|
|
122
|
-
assert.equal(contents("Film", ["Film.mkv", "Film.S01E01.mkv"]).shape, "undetermined");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("two differently named shows are not one series", () => {
|
|
126
|
-
assert.equal(contents("Mixed", ["Alpha.S01E01.mkv", "Beta.S01E01.mkv"]).shape, "undetermined");
|
|
127
|
-
assert.equal(
|
|
128
|
-
contents("Drifters", ["[HorribleSubs] Drifters - 01 [1080p].mkv", "[Other] Drifters - 02 [720p].mkv"]).shape,
|
|
129
|
-
"series"
|
|
130
|
-
);
|
|
131
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What a picture's name says about which episode it is, and what a whole
|
|
3
|
+
* torrent's pictures are taken together.
|
|
4
|
+
*
|
|
5
|
+
* Every name below is taken from a release in the survey collection, except
|
|
6
|
+
* where a test says it is built to reach an edge.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { readEpisodeMarker } from "../services/torrent/episode-naming.js";
|
|
12
|
+
import { TorrentContents } from "../services/torrent/Contents.js";
|
|
13
|
+
|
|
14
|
+
test("season and episode with the episode title after them", () => {
|
|
15
|
+
assert.deepEqual(readEpisodeMarker({ name: "s01e02_Murder.in.the.Mews.avi", folders: ["Season_01"] }), {
|
|
16
|
+
season: 1,
|
|
17
|
+
episodes: [2],
|
|
18
|
+
part: null,
|
|
19
|
+
special: false,
|
|
20
|
+
showHint: "",
|
|
21
|
+
titleHint: "Murder.in.the.Mews"
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("a separator between the season and the episode is still one marker", () => {
|
|
26
|
+
const marker = readEpisodeMarker({ name: "s03.e01_The.Mysterious.Affair.at.Styles.avi" });
|
|
27
|
+
assert.equal(marker.season, 3);
|
|
28
|
+
assert.deepEqual(marker.episodes, [1]);
|
|
29
|
+
assert.equal(marker.titleHint, "The.Mysterious.Affair.at.Styles");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a part of one episode is its own field and leaves the title", () => {
|
|
33
|
+
const marker = readEpisodeMarker({ name: "s02e01_Peril.at.End.House_Part.1.avi" });
|
|
34
|
+
assert.equal(marker.part, 1);
|
|
35
|
+
assert.equal(marker.titleHint, "Peril.at.End.House");
|
|
36
|
+
assert.deepEqual(marker.episodes, [1]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("the show name comes before the marker and release tags after it", () => {
|
|
40
|
+
const marker = readEpisodeMarker({ name: "Reacher.S04E01.1080p.rus.LostFilm.TV.mkv" });
|
|
41
|
+
assert.equal(marker.season, 4);
|
|
42
|
+
assert.equal(marker.showHint, "Reacher");
|
|
43
|
+
assert.equal(marker.titleHint, "1080p.rus.LostFilm.TV");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("one file carrying several episodes lists all of them", () => {
|
|
47
|
+
assert.deepEqual(readEpisodeMarker({ name: "Show.S01E01E02.mkv" }).episodes, [1, 2]);
|
|
48
|
+
assert.deepEqual(readEpisodeMarker({ name: "Show.S01E01-E03.mkv" }).episodes, [1, 2, 3]);
|
|
49
|
+
assert.deepEqual(readEpisodeMarker({ name: "Show.S01E01-02.mkv" }).episodes, [1, 2]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a claim of more episodes than a file carries is not a marker", () => {
|
|
53
|
+
assert.equal(readEpisodeMarker({ name: "Show.S01E01-40.mkv" }), null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("season 0 and a name calling itself a special are specials", () => {
|
|
57
|
+
assert.equal(readEpisodeMarker({ name: "Show.S00E03.mkv" }).special, true);
|
|
58
|
+
assert.equal(readEpisodeMarker({ name: "Show - 05 Special.mkv" }).special, true);
|
|
59
|
+
assert.equal(readEpisodeMarker({ name: "Show.S01E03.mkv" }).special, false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the numbering anime releases use, with no season in it", () => {
|
|
63
|
+
const marker = readEpisodeMarker({ name: "[HorribleSubs] Drifters - 01 [1080p].mkv" });
|
|
64
|
+
assert.equal(marker.season, null);
|
|
65
|
+
assert.deepEqual(marker.episodes, [1]);
|
|
66
|
+
assert.equal(marker.showHint, "[HorribleSubs] Drifters");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a season stated just before the dash number is read", () => {
|
|
70
|
+
const marker = readEpisodeMarker({ name: "Koukaku Kidoutai (2026) S1 - 01.mkv" });
|
|
71
|
+
assert.equal(marker.season, 1);
|
|
72
|
+
assert.equal(marker.showHint, "Koukaku Kidoutai (2026)");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a season folder gives the season to an episode number without one", () => {
|
|
76
|
+
const marker = readEpisodeMarker({ name: "Episode.05.mkv", folders: ["Сезон 2"] });
|
|
77
|
+
assert.equal(marker.season, 2);
|
|
78
|
+
assert.deepEqual(marker.episodes, [5]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("names that state no episode say nothing", () => {
|
|
82
|
+
for (const name of [
|
|
83
|
+
"Despicable.Me.4.2024.1080p.BluRay.x264-EniaHD.mkv",
|
|
84
|
+
"Movie - 2024 [1080p].mkv",
|
|
85
|
+
"WALL-E.2008.1080p.mkv",
|
|
86
|
+
"Mortal.Kombat.II.1080p.rus.LostFilm.TV.mkv",
|
|
87
|
+
"Video.1920x1080.mkv",
|
|
88
|
+
"VTS_01_1.VOB"
|
|
89
|
+
]) {
|
|
90
|
+
assert.equal(readEpisodeMarker({ name }), null, name);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} name
|
|
96
|
+
* @param {string[]} relatives
|
|
97
|
+
* @returns {TorrentContents}
|
|
98
|
+
*/
|
|
99
|
+
function contents(name, relatives) {
|
|
100
|
+
return new TorrentContents({
|
|
101
|
+
name,
|
|
102
|
+
files: relatives.map((relative) => ({
|
|
103
|
+
path: `${name}/${relative}`,
|
|
104
|
+
name: relative.slice(relative.lastIndexOf("/") + 1),
|
|
105
|
+
length: 1
|
|
106
|
+
}))
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
test("one picture is a single work", () => {
|
|
111
|
+
assert.equal(new TorrentContents({ name: "Film.mkv", files: [{ path: "Film.mkv", name: "Film.mkv", length: 1 }] }).shape, "single");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("numbered pictures of one show are a series, and an unnumbered one stays in it", () => {
|
|
115
|
+
const pack = contents("Poirot", ["Season_01/s01e01_A.avi", "Season_01/s01e02_B.avi", "Special.avi"]);
|
|
116
|
+
assert.equal(pack.shape, "series");
|
|
117
|
+
assert.equal(pack.items.find((item) => item.name === "Special.avi").episode, null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("pictures without numbers are not called a collection, only not known", () => {
|
|
121
|
+
assert.equal(contents("Trilogy", ["One.2010.mkv", "Two.2013.mkv", "Three.2017.mkv"]).shape, "undetermined");
|
|
122
|
+
assert.equal(contents("Film", ["Film.mkv", "Film.S01E01.mkv"]).shape, "undetermined");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("two differently named shows are not one series", () => {
|
|
126
|
+
assert.equal(contents("Mixed", ["Alpha.S01E01.mkv", "Beta.S01E01.mkv"]).shape, "undetermined");
|
|
127
|
+
assert.equal(
|
|
128
|
+
contents("Drifters", ["[HorribleSubs] Drifters - 01 [1080p].mkv", "[Other] Drifters - 02 [720p].mkv"]).shape,
|
|
129
|
+
"series"
|
|
130
|
+
);
|
|
131
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"description":"MP4 metadata boxes captured from adjacent AAC segments. Media payloads are omitted.","files":{"init.mp4":"AAAAHGZ0eXBpc281AAACAGlzbzVpc282bXA0MQAAAvRtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAB9nRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAAAAAAAAAAEAAAAAAVttZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAAEGbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAADKc3RibAAAAH5zdHNkAAAAAAAAAAEAAABubXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAA2ZXNkcwAAAAADgICAJQABAASAgIAXQBUAAAAAAfQAAAH0AAWAgIAFEZBW5QAGgICAAQIAAAAUYnRydAAAAAAAAfQAAAH0AAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAAE3VkdGEAAAALbmFtZVJVUwAAAChtdmV4AAAAIHRyZXgAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjYyLjEyLjEwMg==","segment-00171.mp4":"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","segment-00172.mp4":"AAAIAG1vb2YAAAAQbWZoZAAAAAAAAAABAAAH6HRyYWYAAAAcdGZoZAACADgAAAABAAAEAAAAARgCAAAAAAAAFHRmZHQBAAAAAAAAAAUhjaAAAAewdHJ1bgAAAgEAAAHnAAAICAAAARgAAAEIAAABGQAAATIAAAFaAAABUwAAAVEAAAFVAAABaQAAAVsAAAFfAAABRQAAAWYAAAFcAAABSwAAAVQAAAFOAAABVAAAAVMAAAFcAAABSwAAAU4AAAE+AAABLwAAATsAAAFQAAABUAAAAUYAAAFUAAABVAAAAVUAAAFsAAABeAAAATgAAAFuAAABRwAAAVoAAAFJAAABRgAAAVUAAAF9AAABNQAAAVgAAAF3AAABQgAAAVIAAAFQAAABWwAAAV4AAAEtAAABWQAAAWsAAAFEAAABcAAAAUUAAAFdAAABZQAAAWAAAAFiAAABQgAAAVEAAAFQAAABKwAAAV4AAAFTAAABbQAAAYIAAAEyAAABdQAAAUAAAAFSAAABbQAAAUoAAAFtAAABtgAAAPwAAAEwAAABewAAAT4AAAFLAAABRQAAAWAAAAFtAAABXQAAASsAAAFyAAABZAAAATEAAAFYAAABcwAAAU8AAAFaAAABSAAAAWYAAAFDAAABOwAAAWMAAAFxAAABQQAAAWgAAAF2AAABlAAAAQkAAAFFAAABMAAAAW8AAAFSAAABUgAAAVIAAAFOAAABfgAAAT4AAAFUAAABPAAAAXsAAAE2AAABUAAAAXAAAAFUAAABUwAAAXAAAAE6AAABQQAAAVsAAAFVAAABVgAAAWoAAAFMAAABQwAAAU0AAAFWAAABTAAAAXAAAAFbAAABVAAAAW4AAAE9AAABWAAAAWUAAAFBAAABXgAAAU8AAAFfAAABUwAAAVoAAAFLAAABaAAAAVgAAAFMAAABXAAAAU4AAAFjAAABRQAAAV8AAAFbAAABUgAAAU8AAAFeAAABSQAAAVkAAAFXAAABPQAAAWgAAAFRAAABVwAAAW0AAAE5AAABYAAAAWkAAAFFAAABXgAAAVgAAAFPAAABNwAAAUwAAAGdAAABHwAAAWEAAAF0AAABUgAAAScAAAF3AAABcAAAAVAAAAE5AAABVgAAAWcAAAFSAAABdAAAAU0AAAFpAAABMgAAAWsAAAFrAAABXQAAAU4AAAFiAAABSAAAAWgAAAFNAAABPAAAAUQAAAF7AAABQwAAAWEAAAFLAAABcQAAAUcAAAFfAAABVQAAAUkAAAFJAAABVwAAAVgAAAFaAAABSAAAAXUAAAFHAAABWwAAAWcAAAFhAAABPAAAAVgAAAFbAAABWwAAAVMAAAFZAAABVQAAAVgAAAFEAAABWwAAAUkAAAFcAAABVgAAAWsAAAE7AAABegAAAU4AAAFQAAABSgAAAUoAAAFaAAABTAAAAVoAAAFuAAABVAAAAUUAAAFQAAABaAAAAU8AAAFYAAABUAAAAVEAAAE8AAABeQAAAVIAAAFnAAABWQAAAVUAAAFfAAABUAAAAWUAAAE/AAABYgAAAUwAAAFPAAABYQAAAWcAAAFDAAABUwAAAVIAAAFPAAABXQAAAV4AAAFRAAABUAAAAUEAAAFTAAABXAAAAWwAAAFJAAABWAAAAVkAAAFTAAABVQAAAVYAAAFKAAABZgAAAVQAAAFlAAABUgAAAUwAAAFiAAABWQAAAU0AAAFRAAABZgAAATYAAAFoAAABZAAAAUgAAAFeAAABUQAAAU4AAAFgAAABVQAAAVsAAAFLAAABQgAAAVgAAAFJAAABMwAAAU8AAAFwAAABOQAAAX8AAAE7AAABaAAAAXIAAAFBAAABWgAAAU4AAAFTAAABagAAAT4AAAF3AAABZQAAAUQAAAFbAAABXwAAAUQAAAF6AAABVgAAAU0AAAFQAAABZAAAAT0AAAFsAAABTgAAAWcAAAFTAAABWQAAAUwAAAF5AAABWAAAAVkAAAE2AAABcwAAAUcAAAFrAAABQgAAAV8AAAE8AAABdQAAAVoAAAFGAAABUQAAAVwAAAFyAAABPQAAAUsAAAFPAAABTgAAAU4AAAFVAAABcAAAASwAAAGCAAABLAAAAXEAAAFRAAABZwAAAVIAAAE+AAABYwAAAXUAAAFrAAABUgAAAV4AAAEyAAABJQAAAXkAAAE+AAABpAAAATAAAAE9AAABXQAAAScAAAEzAAABLAAAAWAAAAE8AAABTwAAAUoAAAFCAAABSAAAAU8AAAFJAAABkgAAAVcAAAE9AAABQgAAAWIAAAFbAAABTAAAAU4AAAF7AAABawAAAVsAAAFXAAABNwAAAU4AAAFPAAABOAAAAXUAAAFjAAABTgAAAVQAAAFGAAABRAAAAVEAAAFkAAABQQAAAU0AAAFgAAABWwAAAYMAAAFFAAABWgAAAWUAAAFTAAABUgAAATkAAAF/AAABOAAAAUQAAAFFAAABSQAAAVIAAAFcAAABhgAAAVcAAAFZAAABRQAAAWQAAAFaAAABTwAAAV4AAAFcAAABVAAAAVYAAAFjAAABRQAAAUQAAAFiAAABVQAAAV8AAAF0AAABawAAAVMAAAFhAAABVAAAATsAAAE9AAABYwAAAVAAAAFWAAABbQAAAVUAAAFUAAABdgAAAU4AAAFGAAABYgAAAUwAAAFLAAABXQAAAUcAAAFYAAABWAAAAW8AAAFdAAABYQAAAVMAAAGgAAACQQAAAYs="},"browserBufferedRanges":[[1782.997,1793.428999],[1782.997,1803.827333]]}
|
|
1
|
+
{"description":"MP4 metadata boxes captured from adjacent AAC segments. Media payloads are omitted.","files":{"init.mp4":"AAAAHGZ0eXBpc281AAACAGlzbzVpc282bXA0MQAAAvRtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAB9nRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAAAAAAAAAAEAAAAAAVttZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAAEGbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAADKc3RibAAAAH5zdHNkAAAAAAAAAAEAAABubXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAA2ZXNkcwAAAAADgICAJQABAASAgIAXQBUAAAAAAfQAAAH0AAWAgIAFEZBW5QAGgICAAQIAAAAUYnRydAAAAAAAAfQAAAH0AAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAAE3VkdGEAAAALbmFtZVJVUwAAAChtdmV4AAAAIHRyZXgAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjYyLjEyLjEwMg==","segment-00171.mp4":"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","segment-00172.mp4":"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"},"browserBufferedRanges":[[1782.997,1793.428999],[1782.997,1803.827333]]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
4
|
-
import { continuePresentationRanges, readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
4
|
+
import { continuePresentationRanges, presentationCoverageEnd, readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
5
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
@@ -53,6 +53,12 @@ function fragment(decodeTime, composition) {
|
|
|
53
53
|
])));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
test("an interrupted frame cannot prove coverage of a full future cut", () => {
|
|
57
|
+
const ranges = readPresentationRanges(Buffer.concat([init(456000, 0), fragment(0, 0)]));
|
|
58
|
+
assert.ok(presentationCoverageEnd(ranges) > 456);
|
|
59
|
+
assert.ok(presentationCoverageEnd(ranges) < 456.1);
|
|
60
|
+
});
|
|
61
|
+
|
|
56
62
|
test("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
57
63
|
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
58
64
|
const own = init(4125, 3000);
|
|
@@ -69,6 +75,14 @@ test("a shared init preserves presentation times without adding composition dela
|
|
|
69
75
|
assert.deepEqual(times, [98000, 146000]);
|
|
70
76
|
});
|
|
71
77
|
|
|
78
|
+
test("video exposes its coded-frame join boundary without changing its measured end", () => {
|
|
79
|
+
const header = init(0, 0, "vide");
|
|
80
|
+
const ranges = readPresentationRanges(Buffer.concat([header, fragment(24000, 0)]));
|
|
81
|
+
const translated = fmp4Format.clientMediaRanges(ranges, { initBytes: header, timestampOffsetSeconds: 0 });
|
|
82
|
+
assert.equal(translated[0].end, ranges[0].end);
|
|
83
|
+
assert.ok(Math.abs(translated[0].joinEnd - (1 + 2 * (1000 / 24000))) <= Number.EPSILON);
|
|
84
|
+
});
|
|
85
|
+
|
|
72
86
|
test("a shared init first read after a seek also supports returning to the beginning", () => {
|
|
73
87
|
const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
|
|
74
88
|
const media = fragment(0, 2000);
|
|
@@ -124,7 +138,8 @@ test("client clock projection accounts for shared edits and audio initPTS", () =
|
|
|
124
138
|
const projected = fmp4Format.clientMediaRanges(video, {
|
|
125
139
|
initBytes: videoHeader, timestampOffsetSeconds: -2000 / 24000
|
|
126
140
|
});
|
|
127
|
-
assert.deepEqual(projected, video);
|
|
141
|
+
assert.deepEqual(projected.map(({ start, end }) => ({ start, end })), video);
|
|
142
|
+
assert.ok(projected[0].joinEnd > projected[0].end);
|
|
128
143
|
});
|
|
129
144
|
|
|
130
145
|
test("a variable-rate frame crosses a cut only with continuous decode samples", () => {
|
package/test/link-budget.test.js
CHANGED
|
@@ -9,12 +9,12 @@ import test from "node:test";
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import {
|
|
11
11
|
admissionRank,
|
|
12
|
-
audioLoadOf,
|
|
13
12
|
LINK_SAFETY,
|
|
14
13
|
LINK_VERDICT,
|
|
15
14
|
linkCouldCarry,
|
|
16
15
|
loadOf,
|
|
17
16
|
PEAK_CLASS,
|
|
17
|
+
soundtrackLoadOf,
|
|
18
18
|
videoLoadForFrame,
|
|
19
19
|
videoLoadOfSpec
|
|
20
20
|
} from "../services/encode/quality/link-budget.js";
|
|
@@ -51,11 +51,12 @@ test("an output's picture is read off its own rate control", () => {
|
|
|
51
51
|
assert.deepEqual(videoLoadOfSpec(outputSpec(), 3.73), estimated(3.73), "a copy is its source's average");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
test("a soundtrack is
|
|
55
|
-
assert.deepEqual(
|
|
56
|
-
assert.deepEqual(
|
|
57
|
-
assert.deepEqual(
|
|
58
|
-
assert.
|
|
54
|
+
test("a soundtrack is estimated when re-encoded or copied at a stated rate, known at its codec's bound, unknown when nothing states one", () => {
|
|
55
|
+
assert.deepEqual(soundtrackLoadOf({ bitrateKbps: null }, true), estimated(AUDIO_TRANSCODE_KBPS / 1000));
|
|
56
|
+
assert.deepEqual(soundtrackLoadOf({ bitrateKbps: 640 }, false), estimated(0.64));
|
|
57
|
+
assert.deepEqual(soundtrackLoadOf({ bitrateKbps: 640, peakKbps: 640 }, false), known(0.64));
|
|
58
|
+
assert.deepEqual(soundtrackLoadOf({ bitrateKbps: null }, false), unknown());
|
|
59
|
+
assert.equal(soundtrackLoadOf(null, null), null, "no sound, no part");
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
test("a load is as trustworthy as its least trustworthy part", () => {
|