@torrent-tv/proxy 2.88.5 → 2.89.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.
Files changed (50) hide show
  1. package/AGENTS.md +21 -12
  2. package/CHANGELOG.md +18 -0
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/docs/container-architecture.md +77 -22
  6. package/docs/playback-start-forecast.md +56 -56
  7. package/main2.cpuprofile +1 -0
  8. package/package.json +1 -1
  9. package/scripts/build-audio-samples.mjs +31 -31
  10. package/server.js +42 -27
  11. package/services/encode/EncodeRuns.js +3 -1
  12. package/services/encode/OutputOpening.js +5 -4
  13. package/services/encode/audio-calibration.js +67 -67
  14. package/services/encode/audio-work-rate.js +15 -15
  15. package/services/encode/output/index.js +4 -0
  16. package/services/encode/run-command.js +2 -1
  17. package/services/media/ContainerOrchestrator.js +99 -46
  18. package/services/media/KeyframeTables.js +61 -21
  19. package/services/media/SubtitleCues.js +255 -183
  20. package/services/media/container/AviContainer.js +5 -3
  21. package/services/media/container/Container.js +66 -43
  22. package/services/media/container/ContainerFactory.js +8 -26
  23. package/services/media/container/MatroskaContainer.js +719 -1182
  24. package/services/media/container/Mp4Container.js +83 -73
  25. package/services/media/container/ebml-stream.js +174 -0
  26. package/services/media/container/matroska-clusters.js +1165 -0
  27. package/services/media/container/unavailable.js +77 -0
  28. package/services/media/tracks/ContainerTrack.js +9 -0
  29. package/services/media/tracks/TextSubtitleTrack.js +9 -1
  30. package/services/server/ViewerRequests.js +3 -2
  31. package/services/server/playback-readiness.js +516 -516
  32. package/services/torrent/worker/client.js +6 -2
  33. package/services/torrent/worker/held-bytes.js +84 -8
  34. package/services/torrent/worker/worker.js +10 -2
  35. package/services/transport/data-channel-handler.js +7 -3
  36. package/services/viewer/PriorityMap.js +31 -0
  37. package/test/audio-calibration.test.js +23 -23
  38. package/test/coalesce.test.js +39 -0
  39. package/test/held-read-ownership.test.js +77 -0
  40. package/test/helpers/held-file.js +50 -2
  41. package/test/helpers/matroska-file.js +296 -0
  42. package/test/keyframe-table-budget.test.js +21 -0
  43. package/test/matroska-cluster-discovery.test.js +323 -0
  44. package/test/reads-not-yet-arrived.test.js +218 -0
  45. package/test/segment-store.test.js +14 -0
  46. package/test/subtitle-cue-walk.test.js +16 -1
  47. package/test/subtitle-plan-waits.test.js +125 -0
  48. package/test/subtitle-track-numbering.test.js +8 -3
  49. package/test/viewer-starts-on-map.test.js +27 -0
  50. package/utils/coalesce.js +41 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @file A read whose bytes have not arrived is not an answer about the file.
3
+ *
4
+ * Field 2026-10-01: the subtitle plan of an episode was read while its Cues
5
+ * table was still downloading, the read was given up, and the plan kept "no
6
+ * clusters" for the life of the process — the track showed nothing for a whole
7
+ * session while the same table, read eleven seconds later for the keyframes,
8
+ * was there. The same shape was in the container choice, the track table, the
9
+ * keyframe table, MP4's `moov` and AVI's `idx1`. Every check here makes the
10
+ * bytes missing first and present afterwards, and asks the same object again.
11
+ *
12
+ * Fakes only: a buffer and a reader over it. No torrent, no thread.
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+
18
+ import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
19
+ import { Mp4Container } from "../services/media/container/Mp4Container.js";
20
+ import { AviContainer } from "../services/media/container/AviContainer.js";
21
+ import { ContainerOrchestrator } from "../services/media/ContainerOrchestrator.js";
22
+ import { KeyframeTables } from "../services/media/KeyframeTables.js";
23
+ import { isUnavailable } from "../services/media/container/unavailable.js";
24
+ import {
25
+ ID,
26
+ buildMatroska,
27
+ clusterData,
28
+ cueBlock,
29
+ element,
30
+ readerOver,
31
+ stringElement,
32
+ trackEntry,
33
+ uintElement
34
+ } from "./helpers/matroska-file.js";
35
+
36
+ const quiet = { info() {}, warn() {} };
37
+
38
+ function episode({ subtitleLanguage = null, cuesBeforeClusters = false, cues = [1], extraTrack = null } = {}) {
39
+ const tracks = [
40
+ trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "jpn" }),
41
+ trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: subtitleLanguage })
42
+ ];
43
+ if (extraTrack) {
44
+ tracks.push(extraTrack);
45
+ }
46
+ return buildMatroska({
47
+ tracks,
48
+ cues,
49
+ cuesBeforeClusters,
50
+ clusters: [
51
+ { ticks: 0, data: clusterData({ ticks: 0, blocks: [cueBlock({ track: 3, relativeTicks: 100, durationTicks: 900, text: "one" })] }) },
52
+ { ticks: 5000, data: clusterData({ ticks: 5000, blocks: [cueBlock({ track: 3, relativeTicks: 0, durationTicks: 900, text: "two" })] }) }
53
+ ]
54
+ });
55
+ }
56
+
57
+ test("a subtitle plan read before the Cues table arrived is not kept; read again, it names the clusters", async () => {
58
+ const { file, cuesAt, clusterAt } = episode({ cues: [1, 3] });
59
+ let tailHere = false;
60
+ const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
61
+ const container = new MatroskaContainer({ readRange: read, fileSize: file.length });
62
+
63
+ await assert.rejects(container.readSubtitlePlan(), (error) => isUnavailable(error));
64
+
65
+ tailHere = true;
66
+ const plan = await container.readSubtitlePlan();
67
+ assert.equal(plan.cuesState, "complete");
68
+ assert.deepEqual(plan.tracks[0].clusterPositions, clusterAt, "both clusters the table names for the track");
69
+ });
70
+
71
+ test("a read that returns fewer bytes than asked is not taken for the element", async () => {
72
+ const { file } = episode();
73
+ const { read } = readerOver(file, { short: true });
74
+ await assert.rejects(new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks(), (error) =>
75
+ isUnavailable(error)
76
+ );
77
+ });
78
+
79
+ test("a head that has not arrived leaves no container behind, and the next ask builds one", async () => {
80
+ const { file } = episode();
81
+ let headHere = false;
82
+ const { read } = readerOver(file, { missing: (start) => !headHere && start === 0 });
83
+ const containers = new ContainerOrchestrator();
84
+ const params = { sourceKey: "s", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
85
+
86
+ await assert.rejects(containers.containerFor(params), (error) => isUnavailable(error));
87
+ assert.equal(containers.known("s", 0), undefined, "nothing was kept for a head that is not here");
88
+ assert.deepEqual(await containers.getTracks(params), [], "the track table answers nothing yet");
89
+
90
+ headHere = true;
91
+ const container = await containers.containerFor(params);
92
+ assert.equal(container.formatName, "matroska");
93
+ assert.equal((await containers.getTracks(params)).length, 2);
94
+ });
95
+
96
+ test("a keyframe table asked for while its Cues are missing stays unanswered, and is filled when they arrive", async () => {
97
+ const { file, cuesAt } = episode();
98
+ let tailHere = false;
99
+ const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
100
+ const containers = new ContainerOrchestrator();
101
+ const params = { sourceKey: "k", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
102
+ const tables = new KeyframeTables({
103
+ readTable: async () => {
104
+ const index = await containers.getKeyframeIndex(params);
105
+ return { times: index?.times ?? null, tolerance: 0, format: "matroska" };
106
+ },
107
+ logger: quiet
108
+ });
109
+
110
+ await assert.rejects(tables.read({ sourceKey: "k", fileIndex: 0 }), (error) => isUnavailable(error));
111
+ const table = tables.of({ sourceKey: "k", fileIndex: 0 });
112
+ assert.equal(table.answered, false, "a read that did not arrive is not an answer");
113
+
114
+ tailHere = true;
115
+ tables.readAgainIfUnanswered({ sourceKey: "k", fileIndex: 0 });
116
+ await tables.read({ sourceKey: "k", fileIndex: 0 });
117
+ assert.equal(table.answered, true);
118
+ assert.deepEqual(table.times, [0, 5], "the same object the sessions hold now carries the times");
119
+ });
120
+
121
+ test("a Cues element stored before the first cluster, with no SeekHead entry, is found", async () => {
122
+ const { file } = episode({ cuesBeforeClusters: true });
123
+ const { read } = readerOver(file);
124
+ const index = await new MatroskaContainer({ readRange: read, fileSize: file.length }).parseKeyframeIndex();
125
+ assert.deepEqual(index.times, [0, 5]);
126
+ });
127
+
128
+ test("a Tracks element larger than the head window is read by its own size", async () => {
129
+ // A CodecPrivate of 80 KB pushes the end of Tracks past the 64 KB head.
130
+ const big = element(
131
+ ID.TRACK_ENTRY,
132
+ Buffer.concat([
133
+ uintElement(ID.TRACK_NUMBER, 4),
134
+ uintElement(ID.TRACK_TYPE, 17),
135
+ stringElement(ID.CODEC_ID, "S_TEXT/ASS"),
136
+ element(0x63a2, Buffer.alloc(80 * 1024, 0x20))
137
+ ])
138
+ );
139
+ const { file } = episode({ extraTrack: big });
140
+ const { read } = readerOver(file);
141
+ const tracks = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
142
+ assert.deepEqual(tracks.map((track) => track.trackNumber), [1, 3, 4]);
143
+ });
144
+
145
+ test("the language: LanguageBCP47 wins, then Language, then the format's default `eng`; `und` stays unknown", async () => {
146
+ const cases = [
147
+ { entry: { language: null }, language: "eng", source: "default" },
148
+ { entry: { language: "" }, language: "eng", source: "language" },
149
+ { entry: { language: "rus" }, language: "rus", source: "language" },
150
+ { entry: { language: null, languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
151
+ { entry: { language: "por", languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
152
+ { entry: { language: "und" }, language: "und", source: "language" }
153
+ ];
154
+ for (const { entry, language, source } of cases) {
155
+ const { file } = buildMatroska({
156
+ tracks: [trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", ...entry })],
157
+ cues: null,
158
+ clusters: [{ ticks: 0, data: clusterData({ ticks: 0, blocks: [] }) }]
159
+ });
160
+ const { read } = readerOver(file);
161
+ const [track] = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
162
+ assert.equal(track.language, language, JSON.stringify(entry));
163
+ assert.equal(track.languageSource, source, JSON.stringify(entry));
164
+ }
165
+ });
166
+
167
+ /** @param {string} type @param {Buffer} payload */
168
+ function box(type, payload) {
169
+ const header = Buffer.alloc(8);
170
+ header.writeUInt32BE(8 + payload.length, 0);
171
+ header.write(type, 4, "latin1");
172
+ return Buffer.concat([header, payload]);
173
+ }
174
+
175
+ test("an MP4 whose `moov` has not arrived keeps nothing; read again, it answers", async () => {
176
+ const mvhd = Buffer.alloc(100);
177
+ mvhd.writeUInt32BE(1000, 12); // timescale
178
+ mvhd.writeUInt32BE(5000, 16); // duration
179
+ const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
180
+ const moov = box("moov", box("mvhd", mvhd));
181
+ const file = Buffer.concat([ftyp, moov]);
182
+ let moovHere = false;
183
+ const { read } = readerOver(file, { missing: (start) => !moovHere && start >= ftyp.length });
184
+ const container = new Mp4Container({ readRange: read, fileSize: file.length });
185
+
186
+ await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
187
+ moovHere = true;
188
+ assert.equal((await container.readMediaInfo()).durationSeconds, 5);
189
+ });
190
+
191
+ test("an MP4 `moov` larger than this reads whole is refused by name, not reported absent", async () => {
192
+ const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
193
+ const header = Buffer.alloc(8);
194
+ header.writeUInt32BE(40 * 1024 * 1024, 0);
195
+ header.write("moov", 4, "latin1");
196
+ const file = Buffer.concat([ftyp, header, Buffer.alloc(64)]);
197
+ const { read } = readerOver(file);
198
+ const container = new Mp4Container({ readRange: read, fileSize: file.length });
199
+ assert.equal(await container.readSubtitlePlan(), null);
200
+ assert.deepEqual(container.moovRefused, { size: 40 * 1024 * 1024, limit: 32 * 1024 * 1024 });
201
+ });
202
+
203
+ test("an AVI head that has not arrived leaves no media info behind", async () => {
204
+ const avih = Buffer.alloc(56);
205
+ avih.writeUInt32LE(40_000, 0); // 25 frames a second
206
+ avih.writeUInt32LE(250, 16); // ten seconds
207
+ const chunk = Buffer.concat([Buffer.from("avih", "latin1"), Buffer.from([56, 0, 0, 0]), avih]);
208
+ const list = Buffer.concat([Buffer.from("LIST", "latin1"), Buffer.alloc(4), Buffer.from("hdrl", "latin1"), chunk]);
209
+ list.writeUInt32LE(list.length - 8, 4);
210
+ const riff = Buffer.concat([Buffer.from("RIFF", "latin1"), Buffer.alloc(4), Buffer.from("AVI ", "latin1"), list]);
211
+ riff.writeUInt32LE(riff.length - 8, 4);
212
+ let here = false;
213
+ const { read } = readerOver(riff, { missing: () => !here });
214
+ const container = new AviContainer({ readRange: read, fileSize: riff.length });
215
+ await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
216
+ here = true;
217
+ assert.equal((await container.readMediaInfo()).durationSeconds, 10);
218
+ });
@@ -10,6 +10,7 @@ import os from "node:os";
10
10
  import path from "node:path";
11
11
  import { directoryNameFor, SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
12
12
  import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
13
+ import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
13
14
 
14
15
  /**
15
16
  * @returns {{ store: SegmentStore, root: string, lines: string[] }}
@@ -35,6 +36,19 @@ function writeSegment(dir, index, bytes = 16) {
35
36
 
36
37
  const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
37
38
 
39
+ test("a complete audio cut within the configured muxer delta remains usable", (t) => {
40
+ const { store, root } = storeInATempRoot();
41
+ t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
42
+ const format = { ...fmp4Format, readMediaRanges: () => [{ start: 0, end: 4.738913832199547 }],
43
+ mediaCoverageEnd: (ranges) => ranges.at(-1).end };
44
+ const dir = store.directoryFor(KEY);
45
+ store.useFormat(KEY, format);
46
+ const endSeconds = 4.755 - SEGMENT_CUT_TIME_DELTA_SECONDS;
47
+ writeFileSync(path.join(dir, "making-0-00000.mp4"), Buffer.alloc(16));
48
+ assert.equal(store.publish(KEY, "making-0-00000.mp4", format, { endSeconds }), "segment-00000.mp4");
49
+ assert.ok(store.mediaRangesOf(KEY, 0, { endSeconds }));
50
+ });
51
+
38
52
  test("a closed but truncated non-final piece is not published or reused", (t) => {
39
53
  const { store, root } = storeInATempRoot();
40
54
  t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
@@ -19,7 +19,7 @@ import test from "node:test";
19
19
  import assert from "node:assert/strict";
20
20
  import { Readable } from "node:stream";
21
21
 
22
- import { cuesHeldFor, warmSubtitleCues, forgetSubtitles } from "../services/media/SubtitleCues.js";
22
+ import { cuesHeldFor, warmSubtitleCues, forgetSubtitles, subtitleTracksOf } from "../services/media/SubtitleCues.js";
23
23
  import { heldFileOver } from "./helpers/held-file.js";
24
24
 
25
25
  const ID_EBML = 0x1a45dfa3;
@@ -335,12 +335,27 @@ test("one walk fills every track, and a second call reads no cluster again", asy
335
335
  }
336
336
  });
337
337
 
338
+ test("the warm pass walks nothing for a file nobody has asked about", async () => {
339
+ const { file } = buildFile();
340
+ const { torrent, reads } = torrentOver(file);
341
+ const sourceKey = "9".repeat(40);
342
+ forgetSubtitles(sourceKey);
343
+ try {
344
+ assert.deepEqual(await warmSubtitleCues(heldFileOver(torrent, 0, sourceKey)), []);
345
+ assert.equal(reads.length, 0, "neither its head nor its Cues table was read for it");
346
+ } finally {
347
+ forgetSubtitles(sourceKey);
348
+ }
349
+ });
350
+
338
351
  test("the warm pass reports what is new, by the number the browser knows", async () => {
339
352
  const { file } = buildFile();
340
353
  const { torrent } = torrentOver(file);
341
354
  const sourceKey = "e".repeat(40);
342
355
  forgetSubtitles(sourceKey);
343
356
  try {
357
+ // A browser asking for a track is what makes a file one the walk follows.
358
+ await subtitleTracksOf(heldFileOver(torrent, 0, sourceKey));
344
359
  const first = await warmSubtitleCues(heldFileOver(torrent, 0, sourceKey));
345
360
  assert.deepEqual(
346
361
  first.map((entry) => [entry.trackIndex, entry.cues.length, entry.language]).sort(),
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @file The subtitle plan is kept only once it has been read; a plan whose
3
+ * read finishes later serves the same pass; and cues the container takes back
4
+ * leave by the numbers they were sent under.
5
+ *
6
+ * Fakes only: a stand-in container and a held file of plain functions.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { cuesHeldFor, forgetSubtitles, subtitleTracksOf, warmSubtitleCues } from "../services/media/SubtitleCues.js";
13
+ import { BytesUnavailable } from "../services/media/container/unavailable.js";
14
+
15
+ const PLAN = {
16
+ tracks: [{ trackNumber: 2, declaredIndex: 0, codecId: "S_TEXT/UTF8", language: "eng", languageSource: "default", clusterPositions: [] }],
17
+ declared: [],
18
+ secondsPerTick: 0.001,
19
+ segmentDataOffset: 0
20
+ };
21
+
22
+ function heldFile(sourceKey, container) {
23
+ return {
24
+ sourceKey,
25
+ fileIndex: 0,
26
+ name: "film.mkv",
27
+ length: 1000,
28
+ container: async () => container(),
29
+ heldRanges: async () => [[0, 999]],
30
+ readHeld: async () => Buffer.alloc(0)
31
+ };
32
+ }
33
+
34
+ test("a plan whose bytes have not arrived is not kept; the next ask reads it", async () => {
35
+ const sourceKey = "p".repeat(40);
36
+ forgetSubtitles(sourceKey);
37
+ let here = false;
38
+ const container = () => {
39
+ if (!here) {
40
+ throw new BytesUnavailable(900, 999, 0);
41
+ }
42
+ return { readSubtitlePlan: async () => PLAN, readHeldCues: async () => ({ found: new Map(), covered: 0, indexed: 0, withdrawn: [] }) };
43
+ };
44
+ try {
45
+ assert.deepEqual(await subtitleTracksOf(heldFile(sourceKey, container)), []);
46
+ here = true;
47
+ const tracks = await subtitleTracksOf(heldFile(sourceKey, container));
48
+ assert.deepEqual(tracks.map((track) => [track.trackNumber, track.language, track.languageSource]), [[2, "eng", "default"]]);
49
+ } finally {
50
+ forgetSubtitles(sourceKey);
51
+ }
52
+ });
53
+
54
+ test("a plan read that finishes later serves the pass that asked, with no further arrival", async () => {
55
+ const sourceKey = "q".repeat(40);
56
+ forgetSubtitles(sourceKey);
57
+ let release;
58
+ let walks = 0;
59
+ const arrived = new Promise((resolve) => {
60
+ release = resolve;
61
+ });
62
+ const container = {
63
+ readSubtitlePlan: async () => {
64
+ await arrived;
65
+ return PLAN;
66
+ },
67
+ // A cluster is read once: the first pass finds the line, later ones nothing.
68
+ readHeldCues: async () => {
69
+ walks += 1;
70
+ return {
71
+ found: walks === 1 ? new Map([[2, [{ startSeconds: 1, endSeconds: 2, text: "line", source: 77 }]]]) : new Map(),
72
+ covered: 1,
73
+ indexed: 1,
74
+ withdrawn: []
75
+ };
76
+ }
77
+ };
78
+ try {
79
+ // The file is one somebody asked about: a browser's seed is under way.
80
+ const seed = cuesHeldFor(heldFile(sourceKey, () => container), 2);
81
+ const warm = warmSubtitleCues(heldFile(sourceKey, () => container));
82
+ release();
83
+ await seed;
84
+ const entries = await warm;
85
+ assert.equal(entries.length, 1, "the one pass pushed what the plan led to");
86
+ assert.deepEqual(entries[0].cues.map((cue) => [cue.text, cue.seq]), [["line", 1]]);
87
+ } finally {
88
+ forgetSubtitles(sourceKey);
89
+ }
90
+ });
91
+
92
+ test("cues read from a position the container withdraws are taken back by their numbers", async () => {
93
+ const sourceKey = "r".repeat(40);
94
+ forgetSubtitles(sourceKey);
95
+ let passNumber = 0;
96
+ const container = {
97
+ readSubtitlePlan: async () => PLAN,
98
+ readHeldCues: async () => {
99
+ passNumber += 1;
100
+ return passNumber === 1
101
+ ? {
102
+ found: new Map([[2, [
103
+ { startSeconds: 1, endSeconds: 2, text: "real", source: 10 },
104
+ { startSeconds: 3, endSeconds: 4, text: "false", source: 50 }
105
+ ]]]),
106
+ covered: 2,
107
+ indexed: 2,
108
+ withdrawn: []
109
+ }
110
+ : { found: new Map(), covered: 1, indexed: 1, withdrawn: [50] };
111
+ }
112
+ };
113
+ try {
114
+ await subtitleTracksOf(heldFile(sourceKey, () => container));
115
+ const first = await warmSubtitleCues(heldFile(sourceKey, () => container));
116
+ assert.deepEqual(first[0].cues.map((cue) => [cue.text, cue.seq]), [["real", 1], ["false", 2]]);
117
+ const second = await warmSubtitleCues(heldFile(sourceKey, () => container));
118
+ assert.deepEqual(second[0].withdrawn, [2], "the false cue is named by the number it was sent under");
119
+ assert.deepEqual(second[0].cues, []);
120
+ const held = await cuesHeldFor(heldFile(sourceKey, () => container), 2);
121
+ assert.deepEqual(held.cues.map((cue) => cue.text), ["real"], "and it is gone from what is held");
122
+ } finally {
123
+ forgetSubtitles(sourceKey);
124
+ }
125
+ });
@@ -374,13 +374,18 @@ test("where the file writes RFC 5646, that is the language", async () => {
374
374
  assert.equal(track.languageBcp47, "pt-BR");
375
375
  });
376
376
 
377
- test("an omitted Matroska Language element stays unstated rather than becoming the RFC default", async () => {
377
+ test("an omitted Matroska Language element is the format's default, `eng`, and says it was not written", async () => {
378
+ // RFC 9559 §5.1.4.1.19 gives Language the default `eng`; RFC 8794 §11.1.19
379
+ // says an element equal to its default need not be written, and a reader MUST
380
+ // read the default. Until 2026-10-01 such a track was offered as Unknown.
378
381
  const file = fileWithFlags();
379
382
  const plan = await MatroskaContainer.readSubtitlePlan(readerOver(file), file.length);
380
383
 
381
384
  const track = plan.tracks.find((entry) => entry.trackNumber === 6);
382
385
  const declared = plan.declared.find((entry) => entry.trackNumber === 6);
383
- assert.equal(track.language, "");
384
- assert.equal(track.declaresLanguage, false);
386
+ assert.equal(track.language, "eng");
387
+ assert.equal(track.languageSource, "default");
388
+ assert.equal(track.declaresLanguage, false, "the element itself was not written");
389
+ assert.equal(declared.language, "eng");
385
390
  assert.equal(declared.declaresLanguage, false);
386
391
  });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @file Where viewers stand, read off a merged priority map — the times the
3
+ * subtitle walk reads first.
4
+ */
5
+
6
+ import test from "node:test";
7
+ import assert from "node:assert/strict";
8
+
9
+ import { emptyMap, mapForViewer, mergeMaps, viewerStartsOn } from "../services/viewer/PriorityMap.js";
10
+
11
+ test("each viewer's position is a step up in the merged map", () => {
12
+ const map = mergeMaps([
13
+ mapForViewer({ atSeconds: 30, durationSeconds: 300, allowanceSeconds: 5 }),
14
+ mapForViewer({ atSeconds: 200, durationSeconds: 300, allowanceSeconds: 5 })
15
+ ]);
16
+ assert.deepEqual(viewerStartsOn(map), [30, 200]);
17
+ });
18
+
19
+ test("a viewer at the very start, a stopped viewer, and nobody", () => {
20
+ assert.deepEqual(viewerStartsOn(mapForViewer({ atSeconds: 0, durationSeconds: 60, allowanceSeconds: 5 })), [0]);
21
+ assert.deepEqual(
22
+ viewerStartsOn(mapForViewer({ atSeconds: 12, durationSeconds: 60, allowanceSeconds: 5, playing: false })),
23
+ [12]
24
+ );
25
+ assert.deepEqual(viewerStartsOn(emptyMap(60)), []);
26
+ assert.deepEqual(viewerStartsOn(null), []);
27
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @file One run of a piece of work per key at a time, and one more after it
3
+ * whenever it was asked for meanwhile.
4
+ *
5
+ * For work that is asked for by events — pieces arriving — and that reads
6
+ * whatever is there when it runs. A request that arrives while a run is going
7
+ * is neither queued nor dropped: it is remembered as "run again", once,
8
+ * because one more run reads everything that arrived during the first. Dropping
9
+ * it lost exactly the run that would have found the last pieces of a file
10
+ * (`research/subtitles-never-appear-2026-10-01.md`).
11
+ */
12
+
13
+ /**
14
+ * @template {unknown[]} A
15
+ * @param {(...args: A) => Promise<void>} work
16
+ * @returns {(key: string, ...args: A) => Promise<void>}
17
+ */
18
+ export function coalescing(work) {
19
+ /** @type {Set<string>} */
20
+ const running = new Set();
21
+ /** The latest arguments a run was asked for with meanwhile. @type {Map<string, A>} */
22
+ const askedAgain = new Map();
23
+ const run = async (key, ...args) => {
24
+ if (running.has(key)) {
25
+ askedAgain.set(key, args);
26
+ return;
27
+ }
28
+ running.add(key);
29
+ try {
30
+ await work(...args);
31
+ } finally {
32
+ running.delete(key);
33
+ const again = askedAgain.get(key);
34
+ if (again) {
35
+ askedAgain.delete(key);
36
+ void run(key, ...again);
37
+ }
38
+ }
39
+ };
40
+ return run;
41
+ }