@torrent-tv/proxy 2.80.19 → 2.81.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 +12 -0
- package/package.json +1 -1
- package/research/double-spawn-2026-09-10.md +171 -0
- package/services/disk/DiskSpace.js +146 -0
- package/services/disk/wire.js +60 -0
- package/services/encode/EncodeRun.js +25 -3
- package/services/encode/SegmentStore.js +137 -9
- package/services/hls-session-manager.js +26 -45
- package/services/orchestrators/EncodeOrchestrator.js +4 -1
- package/services/piece-store/allowance.js +107 -0
- package/services/piece-store/piece-disk-store.js +365 -0
- package/services/piece-store/shared-piece-store.js +1549 -1535
- package/services/torrent-worker/client.js +32 -0
- package/services/torrent-worker/pool-adapter.js +15 -0
- package/services/torrent-worker/protocol.js +9 -0
- package/services/torrent-worker/worker.js +8 -1
- package/services/viewer/positions.js +48 -0
- package/test/audio-inventory.test.js +176 -176
- package/test/auto-quality-step.test.js +514 -514
- package/test/concurrent-cost.test.js +138 -138
- package/test/coverage-follows-the-disk.test.js +191 -191
- package/test/coverage-map.test.js +195 -195
- package/test/declared-tracks.test.js +35 -35
- package/test/disk-space.test.js +138 -0
- package/test/encode-orchestrator.test.js +0 -3
- package/test/encode-run.test.js +5 -12
- package/test/held-request-width.test.js +155 -155
- package/test/helpers/encode-run.js +2 -2
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +281 -220
- package/test/orchestrator-wired.test.js +199 -199
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +267 -0
- package/test/piece-reader.test.js +4 -4
- package/test/piece-store-eviction.test.js +17 -17
- package/test/piece-store-reservations.test.js +20 -1
- package/test/piece-store-slow-disk.test.js +16 -1
- package/test/read-window.test.js +6 -6
- package/test/run-intervals.test.js +100 -100
- package/test/seek-landing.test.js +109 -109
- package/test/segment-store-eviction.test.js +232 -0
- package/test/segments-are-shared.test.js +1 -1
- package/test/shared-piece-store.test.js +12 -12
- package/test/sidecar-naming.test.js +142 -142
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-walk.test.js +369 -369
- package/test/subtitle-defaults.test.js +97 -97
- package/test/subtitle-track-numbering.test.js +370 -370
- package/test/tail-duplication.test.js +167 -167
- package/test/tracks-begin-together.test.js +195 -195
- package/test/two-viewers-one-picture.test.js +374 -374
- package/test/video-facts.test.js +102 -102
- package/test/wedge-certainty.test.js +131 -131
- package/services/piece-store/disk-tier.js +0 -151
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Two axes, kept apart: the framing a CONTAINER puts round a cue, and the
|
|
3
|
-
* markup the CODEC puts inside it.
|
|
4
|
-
*
|
|
5
|
-
* Field 2026-09-03, an English track of an embedded `.mkv`: the viewer was
|
|
6
|
-
* shown `21,0,Default,,0000,0000,0000,,I am the powerful Demon King of the
|
|
7
|
-
* Sixth Heaven.` — the whole dialogue row, fields and all. The cause was one
|
|
8
|
-
* function doing both jobs and deciding which framing it held by counting
|
|
9
|
-
* commas: it expected the ten fields a row has in a FILE, a Matroska block
|
|
10
|
-
* carries nine, so it returned the row untouched.
|
|
11
|
-
*
|
|
12
|
-
* What each check here pins is therefore not "ASS is stripped" but "the right
|
|
13
|
-
* side answers": the container takes off what it put on, and the codec takes
|
|
14
|
-
* off what it put in. The case that matters most is the last one — the same
|
|
15
|
-
* line of dialogue, stored two ways, coming out identical.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
|
|
21
|
-
import { AviContainer } from "../services/container/AviContainer.js";
|
|
22
|
-
import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
|
|
23
|
-
import { Mp4Container } from "../services/container/Mp4Container.js";
|
|
24
|
-
import { SubtitleFileContainer } from "../services/container/SubtitleFileContainer.js";
|
|
25
|
-
import { MarkupKind, TextSubtitleTrack } from "../services/tracks/TextSubtitleTrack.js";
|
|
26
|
-
|
|
27
|
-
/** The line from the field report, as Matroska stores it. */
|
|
28
|
-
const FIELD_BLOCK =
|
|
29
|
-
"21,0,Default,,0000,0000,0000,,I am the powerful Demon King of the Sixth Heaven. Good... Evil...";
|
|
30
|
-
|
|
31
|
-
function block(text) {
|
|
32
|
-
return Buffer.from(text, "utf8");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test("a Matroska ASS block gives up its text and nothing else", () => {
|
|
36
|
-
// Nine fields exactly, and no comma in the dialogue: the shape that defeated
|
|
37
|
-
// the old field count and put the whole row on the viewer's screen.
|
|
38
|
-
assert.equal(
|
|
39
|
-
MatroskaContainer.cueTextOf(block(FIELD_BLOCK), "S_TEXT/ASS"),
|
|
40
|
-
"I am the powerful Demon King of the Sixth Heaven. Good... Evil..."
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("commas in the dialogue are dialogue, not fields", () => {
|
|
45
|
-
assert.equal(
|
|
46
|
-
MatroskaContainer.cueTextOf(block("7,0,Main,Nobunaga,0000,0000,0000,,Yes, of course, my lord"), "S_TEXT/ASS"),
|
|
47
|
-
"Yes, of course, my lord"
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("an SSA block is framed the same way as an ASS one", () => {
|
|
52
|
-
// SSA writes `Marked` where ASS writes `Layer`; the count is the same and so
|
|
53
|
-
// is the answer.
|
|
54
|
-
assert.equal(
|
|
55
|
-
MatroskaContainer.cueTextOf(block("3,,Default,,0000,0000,0000,,Toujours rien."), "S_TEXT/SSA"),
|
|
56
|
-
"Toujours rien."
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("a row too short to hold a text field yields nothing to show", () => {
|
|
61
|
-
assert.equal(MatroskaContainer.cueTextOf(block("21,0,Default,,0000"), "S_TEXT/ASS"), "");
|
|
62
|
-
assert.equal(MatroskaContainer.cueTextOf(block("21,0,Default,,0000,0000,0000,,"), "S_TEXT/ASS"), "");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("a plain-text Matroska block is not touched at all", () => {
|
|
66
|
-
// `S_TEXT/UTF8` has no framing: the block IS the text, commas included.
|
|
67
|
-
const line = "Yes, of course, my lord";
|
|
68
|
-
assert.equal(MatroskaContainer.cueTextOf(block(line), "S_TEXT/UTF8"), line);
|
|
69
|
-
assert.equal(MatroskaContainer.cueTextOf(block(line), "S_TEXT/WEBVTT"), line);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("an MP4 sample is unframed by the MP4, length prefix and all", () => {
|
|
73
|
-
const text = Buffer.from("Yes, of course, my lord", "utf8");
|
|
74
|
-
const sample = Buffer.concat([Buffer.from([0x00, text.length]), text]);
|
|
75
|
-
assert.equal(Mp4Container.cueTextOf(sample, "tx3g"), "Yes, of course, my lord");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("a container with no framing of its own refuses to guess", () => {
|
|
79
|
-
// AVI declares no subtitle tracks, so no cue can reach it. If one ever did,
|
|
80
|
-
// the answer must be an error and not a guess at somebody else's framing.
|
|
81
|
-
assert.throws(() => AviContainer.cueTextOf(block("x"), "S_TEXT/ASS"), /cueTextOf not implemented/);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("ASS markup is taken off wherever the text came from", () => {
|
|
85
|
-
assert.equal(TextSubtitleTrack.plainTextOf("{\\pos(640,620)}Hello{\\i1} there{\\i0}", "S_TEXT/ASS"), "Hello there");
|
|
86
|
-
assert.equal(TextSubtitleTrack.plainTextOf("First\\NSecond\\nThird", "S_TEXT/ASS"), "First\nSecond\nThird");
|
|
87
|
-
assert.equal(TextSubtitleTrack.plainTextOf("Wide\\hspace", "S_TEXT/ASS"), "Wide space");
|
|
88
|
-
// The same text under a codec that has no such markup keeps its characters.
|
|
89
|
-
assert.equal(TextSubtitleTrack.plainTextOf("{\\pos(1,2)}Hello", "S_TEXT/UTF8"), "{\\pos(1,2)}Hello");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("a codec is known by any of its names", () => {
|
|
93
|
-
assert.equal(TextSubtitleTrack.markupKindOf("S_TEXT/ASS"), MarkupKind.ASS);
|
|
94
|
-
assert.equal(TextSubtitleTrack.markupKindOf(".ASS"), MarkupKind.ASS);
|
|
95
|
-
assert.equal(TextSubtitleTrack.markupKindOf("S_TEXT/UTF8"), MarkupKind.NONE);
|
|
96
|
-
assert.equal(TextSubtitleTrack.markupKindOf("wvtt"), MarkupKind.NONE);
|
|
97
|
-
// Unknown: shown as it is, rather than refused.
|
|
98
|
-
assert.equal(TextSubtitleTrack.markupKindOf("S_TEXT/SOMETHING"), MarkupKind.NONE);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("a text track answers for its own markup", () => {
|
|
102
|
-
const track = new TextSubtitleTrack({ trackNumber: 3, declaredIndex: 0, codecId: "S_TEXT/ASS" });
|
|
103
|
-
assert.equal(track.markupKind, MarkupKind.ASS);
|
|
104
|
-
assert.equal(track.plainText("{\\be1}Готовьте лучников"), "Готовьте лучников");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a file states its own column order and is obeyed", () => {
|
|
108
|
-
// This `Format:` has no `Name` column, so the text is the ninth field and not
|
|
109
|
-
// the tenth. A reader that assumed a position would take the effect field as
|
|
110
|
-
// the line — which is the file-side form of the defect this suite is about.
|
|
111
|
-
const file = [
|
|
112
|
-
"[Script Info]",
|
|
113
|
-
"ScriptType: v4.00+",
|
|
114
|
-
"[Events]",
|
|
115
|
-
"Format: Layer, Start, End, Style, MarginL, MarginR, MarginV, Effect, Text",
|
|
116
|
-
"Dialogue: 0,0:00:01.00,0:00:03.50,Default,0000,0000,0000,,Ты видишь их?",
|
|
117
|
-
""
|
|
118
|
-
].join("\n");
|
|
119
|
-
|
|
120
|
-
const cues = new SubtitleFileContainer({ extension: ".ass" }).readCues(file);
|
|
121
|
-
assert.deepEqual(cues, [{ startSeconds: 1, endSeconds: 3.5, text: "Ты видишь их?" }]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("a dialogue row before any Format line is not guessed at", () => {
|
|
125
|
-
const file = ["[Events]", "Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,Ignored", ""].join("\n");
|
|
126
|
-
assert.deepEqual(new SubtitleFileContainer({ extension: ".ass" }).readCues(file), []);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("SubRip cues keep their words and lose their numbering", () => {
|
|
130
|
-
const file = [
|
|
131
|
-
"1",
|
|
132
|
-
"00:00:01,000 --> 00:00:03,500",
|
|
133
|
-
"Ты видишь их?",
|
|
134
|
-
"",
|
|
135
|
-
"2",
|
|
136
|
-
"00:01:02,250 --> 00:01:04,000",
|
|
137
|
-
"<i>Да.</i>",
|
|
138
|
-
"Но не столько вижу.",
|
|
139
|
-
""
|
|
140
|
-
].join("\n");
|
|
141
|
-
|
|
142
|
-
const cues = new SubtitleFileContainer({ extension: ".srt" }).readCues(file);
|
|
143
|
-
assert.deepEqual(cues, [
|
|
144
|
-
{ startSeconds: 1, endSeconds: 3.5, text: "Ты видишь их?" },
|
|
145
|
-
{ startSeconds: 62.25, endSeconds: 64, text: "<i>Да.</i>\nНо не столько вижу." }
|
|
146
|
-
]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("a SubRip file with no blank line between cues does not swallow the ordinal", () => {
|
|
150
|
-
const file = [
|
|
151
|
-
"1",
|
|
152
|
-
"00:00:01,000 --> 00:00:02,000",
|
|
153
|
-
"First",
|
|
154
|
-
"2",
|
|
155
|
-
"00:00:03,000 --> 00:00:04,000",
|
|
156
|
-
"Second"
|
|
157
|
-
].join("\n");
|
|
158
|
-
|
|
159
|
-
const cues = new SubtitleFileContainer({ extension: ".srt" }).readCues(file);
|
|
160
|
-
assert.deepEqual(cues.map((cue) => cue.text), ["First", "Second"]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("a WebVTT file is passed through, not taken apart", () => {
|
|
164
|
-
const file = "WEBVTT\n\nSTYLE\n::cue { color: yellow }\n\nintro\n00:00:01.000 --> 00:00:02.000\nHello\n";
|
|
165
|
-
assert.equal(SubtitleFileContainer.toVtt(file, ".vtt"), file);
|
|
166
|
-
assert.equal(new SubtitleFileContainer({ extension: ".vtt" }).readCues(file), null);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("a format nothing here reads is refused rather than mangled", () => {
|
|
170
|
-
assert.equal(SubtitleFileContainer.toVtt("whatever", ".sup"), null);
|
|
171
|
-
assert.equal(SubtitleFileContainer.toVtt("whatever", ".ttml"), null);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("one line of dialogue, two framings, one result", () => {
|
|
175
|
-
// The whole point of the split. The same ASS line stored in a Matroska block
|
|
176
|
-
// and in a file must reach the viewer identically, and neither path may know
|
|
177
|
-
// anything about the other.
|
|
178
|
-
const spoken = "Yes, of course, my lord";
|
|
179
|
-
const markup = `{\\pos(640,620)}${spoken}`;
|
|
180
|
-
|
|
181
|
-
const fromBlock = TextSubtitleTrack.finalizeCues(
|
|
182
|
-
[{
|
|
183
|
-
startSeconds: 1,
|
|
184
|
-
endSeconds: 3,
|
|
185
|
-
text: MatroskaContainer.cueTextOf(block(`21,0,Default,,0000,0000,0000,,${markup}`), "S_TEXT/ASS")
|
|
186
|
-
}],
|
|
187
|
-
"S_TEXT/ASS"
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const fromFile = new SubtitleFileContainer({ extension: ".ass" }).readCues([
|
|
191
|
-
"[Events]",
|
|
192
|
-
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
193
|
-
`Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0000,0000,0000,,${markup}`,
|
|
194
|
-
""
|
|
195
|
-
].join("\n"));
|
|
196
|
-
|
|
197
|
-
assert.equal(fromBlock[0].text, spoken);
|
|
198
|
-
assert.equal(TextSubtitleTrack.finalizeCues(fromFile, ".ass")[0].text, spoken);
|
|
199
|
-
assert.equal(TextSubtitleTrack.cuesToVtt(fromBlock, "S_TEXT/ASS"), TextSubtitleTrack.cuesToVtt(fromFile, ".ass"));
|
|
200
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Two axes, kept apart: the framing a CONTAINER puts round a cue, and the
|
|
3
|
+
* markup the CODEC puts inside it.
|
|
4
|
+
*
|
|
5
|
+
* Field 2026-09-03, an English track of an embedded `.mkv`: the viewer was
|
|
6
|
+
* shown `21,0,Default,,0000,0000,0000,,I am the powerful Demon King of the
|
|
7
|
+
* Sixth Heaven.` — the whole dialogue row, fields and all. The cause was one
|
|
8
|
+
* function doing both jobs and deciding which framing it held by counting
|
|
9
|
+
* commas: it expected the ten fields a row has in a FILE, a Matroska block
|
|
10
|
+
* carries nine, so it returned the row untouched.
|
|
11
|
+
*
|
|
12
|
+
* What each check here pins is therefore not "ASS is stripped" but "the right
|
|
13
|
+
* side answers": the container takes off what it put on, and the codec takes
|
|
14
|
+
* off what it put in. The case that matters most is the last one — the same
|
|
15
|
+
* line of dialogue, stored two ways, coming out identical.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
|
|
21
|
+
import { AviContainer } from "../services/container/AviContainer.js";
|
|
22
|
+
import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
|
|
23
|
+
import { Mp4Container } from "../services/container/Mp4Container.js";
|
|
24
|
+
import { SubtitleFileContainer } from "../services/container/SubtitleFileContainer.js";
|
|
25
|
+
import { MarkupKind, TextSubtitleTrack } from "../services/tracks/TextSubtitleTrack.js";
|
|
26
|
+
|
|
27
|
+
/** The line from the field report, as Matroska stores it. */
|
|
28
|
+
const FIELD_BLOCK =
|
|
29
|
+
"21,0,Default,,0000,0000,0000,,I am the powerful Demon King of the Sixth Heaven. Good... Evil...";
|
|
30
|
+
|
|
31
|
+
function block(text) {
|
|
32
|
+
return Buffer.from(text, "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("a Matroska ASS block gives up its text and nothing else", () => {
|
|
36
|
+
// Nine fields exactly, and no comma in the dialogue: the shape that defeated
|
|
37
|
+
// the old field count and put the whole row on the viewer's screen.
|
|
38
|
+
assert.equal(
|
|
39
|
+
MatroskaContainer.cueTextOf(block(FIELD_BLOCK), "S_TEXT/ASS"),
|
|
40
|
+
"I am the powerful Demon King of the Sixth Heaven. Good... Evil..."
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("commas in the dialogue are dialogue, not fields", () => {
|
|
45
|
+
assert.equal(
|
|
46
|
+
MatroskaContainer.cueTextOf(block("7,0,Main,Nobunaga,0000,0000,0000,,Yes, of course, my lord"), "S_TEXT/ASS"),
|
|
47
|
+
"Yes, of course, my lord"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("an SSA block is framed the same way as an ASS one", () => {
|
|
52
|
+
// SSA writes `Marked` where ASS writes `Layer`; the count is the same and so
|
|
53
|
+
// is the answer.
|
|
54
|
+
assert.equal(
|
|
55
|
+
MatroskaContainer.cueTextOf(block("3,,Default,,0000,0000,0000,,Toujours rien."), "S_TEXT/SSA"),
|
|
56
|
+
"Toujours rien."
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a row too short to hold a text field yields nothing to show", () => {
|
|
61
|
+
assert.equal(MatroskaContainer.cueTextOf(block("21,0,Default,,0000"), "S_TEXT/ASS"), "");
|
|
62
|
+
assert.equal(MatroskaContainer.cueTextOf(block("21,0,Default,,0000,0000,0000,,"), "S_TEXT/ASS"), "");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a plain-text Matroska block is not touched at all", () => {
|
|
66
|
+
// `S_TEXT/UTF8` has no framing: the block IS the text, commas included.
|
|
67
|
+
const line = "Yes, of course, my lord";
|
|
68
|
+
assert.equal(MatroskaContainer.cueTextOf(block(line), "S_TEXT/UTF8"), line);
|
|
69
|
+
assert.equal(MatroskaContainer.cueTextOf(block(line), "S_TEXT/WEBVTT"), line);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("an MP4 sample is unframed by the MP4, length prefix and all", () => {
|
|
73
|
+
const text = Buffer.from("Yes, of course, my lord", "utf8");
|
|
74
|
+
const sample = Buffer.concat([Buffer.from([0x00, text.length]), text]);
|
|
75
|
+
assert.equal(Mp4Container.cueTextOf(sample, "tx3g"), "Yes, of course, my lord");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a container with no framing of its own refuses to guess", () => {
|
|
79
|
+
// AVI declares no subtitle tracks, so no cue can reach it. If one ever did,
|
|
80
|
+
// the answer must be an error and not a guess at somebody else's framing.
|
|
81
|
+
assert.throws(() => AviContainer.cueTextOf(block("x"), "S_TEXT/ASS"), /cueTextOf not implemented/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("ASS markup is taken off wherever the text came from", () => {
|
|
85
|
+
assert.equal(TextSubtitleTrack.plainTextOf("{\\pos(640,620)}Hello{\\i1} there{\\i0}", "S_TEXT/ASS"), "Hello there");
|
|
86
|
+
assert.equal(TextSubtitleTrack.plainTextOf("First\\NSecond\\nThird", "S_TEXT/ASS"), "First\nSecond\nThird");
|
|
87
|
+
assert.equal(TextSubtitleTrack.plainTextOf("Wide\\hspace", "S_TEXT/ASS"), "Wide space");
|
|
88
|
+
// The same text under a codec that has no such markup keeps its characters.
|
|
89
|
+
assert.equal(TextSubtitleTrack.plainTextOf("{\\pos(1,2)}Hello", "S_TEXT/UTF8"), "{\\pos(1,2)}Hello");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a codec is known by any of its names", () => {
|
|
93
|
+
assert.equal(TextSubtitleTrack.markupKindOf("S_TEXT/ASS"), MarkupKind.ASS);
|
|
94
|
+
assert.equal(TextSubtitleTrack.markupKindOf(".ASS"), MarkupKind.ASS);
|
|
95
|
+
assert.equal(TextSubtitleTrack.markupKindOf("S_TEXT/UTF8"), MarkupKind.NONE);
|
|
96
|
+
assert.equal(TextSubtitleTrack.markupKindOf("wvtt"), MarkupKind.NONE);
|
|
97
|
+
// Unknown: shown as it is, rather than refused.
|
|
98
|
+
assert.equal(TextSubtitleTrack.markupKindOf("S_TEXT/SOMETHING"), MarkupKind.NONE);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a text track answers for its own markup", () => {
|
|
102
|
+
const track = new TextSubtitleTrack({ trackNumber: 3, declaredIndex: 0, codecId: "S_TEXT/ASS" });
|
|
103
|
+
assert.equal(track.markupKind, MarkupKind.ASS);
|
|
104
|
+
assert.equal(track.plainText("{\\be1}Готовьте лучников"), "Готовьте лучников");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a file states its own column order and is obeyed", () => {
|
|
108
|
+
// This `Format:` has no `Name` column, so the text is the ninth field and not
|
|
109
|
+
// the tenth. A reader that assumed a position would take the effect field as
|
|
110
|
+
// the line — which is the file-side form of the defect this suite is about.
|
|
111
|
+
const file = [
|
|
112
|
+
"[Script Info]",
|
|
113
|
+
"ScriptType: v4.00+",
|
|
114
|
+
"[Events]",
|
|
115
|
+
"Format: Layer, Start, End, Style, MarginL, MarginR, MarginV, Effect, Text",
|
|
116
|
+
"Dialogue: 0,0:00:01.00,0:00:03.50,Default,0000,0000,0000,,Ты видишь их?",
|
|
117
|
+
""
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
const cues = new SubtitleFileContainer({ extension: ".ass" }).readCues(file);
|
|
121
|
+
assert.deepEqual(cues, [{ startSeconds: 1, endSeconds: 3.5, text: "Ты видишь их?" }]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a dialogue row before any Format line is not guessed at", () => {
|
|
125
|
+
const file = ["[Events]", "Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,Ignored", ""].join("\n");
|
|
126
|
+
assert.deepEqual(new SubtitleFileContainer({ extension: ".ass" }).readCues(file), []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("SubRip cues keep their words and lose their numbering", () => {
|
|
130
|
+
const file = [
|
|
131
|
+
"1",
|
|
132
|
+
"00:00:01,000 --> 00:00:03,500",
|
|
133
|
+
"Ты видишь их?",
|
|
134
|
+
"",
|
|
135
|
+
"2",
|
|
136
|
+
"00:01:02,250 --> 00:01:04,000",
|
|
137
|
+
"<i>Да.</i>",
|
|
138
|
+
"Но не столько вижу.",
|
|
139
|
+
""
|
|
140
|
+
].join("\n");
|
|
141
|
+
|
|
142
|
+
const cues = new SubtitleFileContainer({ extension: ".srt" }).readCues(file);
|
|
143
|
+
assert.deepEqual(cues, [
|
|
144
|
+
{ startSeconds: 1, endSeconds: 3.5, text: "Ты видишь их?" },
|
|
145
|
+
{ startSeconds: 62.25, endSeconds: 64, text: "<i>Да.</i>\nНо не столько вижу." }
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a SubRip file with no blank line between cues does not swallow the ordinal", () => {
|
|
150
|
+
const file = [
|
|
151
|
+
"1",
|
|
152
|
+
"00:00:01,000 --> 00:00:02,000",
|
|
153
|
+
"First",
|
|
154
|
+
"2",
|
|
155
|
+
"00:00:03,000 --> 00:00:04,000",
|
|
156
|
+
"Second"
|
|
157
|
+
].join("\n");
|
|
158
|
+
|
|
159
|
+
const cues = new SubtitleFileContainer({ extension: ".srt" }).readCues(file);
|
|
160
|
+
assert.deepEqual(cues.map((cue) => cue.text), ["First", "Second"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a WebVTT file is passed through, not taken apart", () => {
|
|
164
|
+
const file = "WEBVTT\n\nSTYLE\n::cue { color: yellow }\n\nintro\n00:00:01.000 --> 00:00:02.000\nHello\n";
|
|
165
|
+
assert.equal(SubtitleFileContainer.toVtt(file, ".vtt"), file);
|
|
166
|
+
assert.equal(new SubtitleFileContainer({ extension: ".vtt" }).readCues(file), null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a format nothing here reads is refused rather than mangled", () => {
|
|
170
|
+
assert.equal(SubtitleFileContainer.toVtt("whatever", ".sup"), null);
|
|
171
|
+
assert.equal(SubtitleFileContainer.toVtt("whatever", ".ttml"), null);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("one line of dialogue, two framings, one result", () => {
|
|
175
|
+
// The whole point of the split. The same ASS line stored in a Matroska block
|
|
176
|
+
// and in a file must reach the viewer identically, and neither path may know
|
|
177
|
+
// anything about the other.
|
|
178
|
+
const spoken = "Yes, of course, my lord";
|
|
179
|
+
const markup = `{\\pos(640,620)}${spoken}`;
|
|
180
|
+
|
|
181
|
+
const fromBlock = TextSubtitleTrack.finalizeCues(
|
|
182
|
+
[{
|
|
183
|
+
startSeconds: 1,
|
|
184
|
+
endSeconds: 3,
|
|
185
|
+
text: MatroskaContainer.cueTextOf(block(`21,0,Default,,0000,0000,0000,,${markup}`), "S_TEXT/ASS")
|
|
186
|
+
}],
|
|
187
|
+
"S_TEXT/ASS"
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const fromFile = new SubtitleFileContainer({ extension: ".ass" }).readCues([
|
|
191
|
+
"[Events]",
|
|
192
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
193
|
+
`Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0000,0000,0000,,${markup}`,
|
|
194
|
+
""
|
|
195
|
+
].join("\n"));
|
|
196
|
+
|
|
197
|
+
assert.equal(fromBlock[0].text, spoken);
|
|
198
|
+
assert.equal(TextSubtitleTrack.finalizeCues(fromFile, ".ass")[0].text, spoken);
|
|
199
|
+
assert.equal(TextSubtitleTrack.cuesToVtt(fromBlock, "S_TEXT/ASS"), TextSubtitleTrack.cuesToVtt(fromFile, ".ass"));
|
|
200
|
+
});
|