@torrent-tv/proxy 2.89.6 → 2.89.8

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 (92) hide show
  1. package/AGENTS.md +32 -12
  2. package/CHANGELOG.md +2340 -2330
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/biome.json +412 -412
  6. package/docs/browser-proxy-contract.md +155 -155
  7. package/docs/container-architecture.md +273 -273
  8. package/docs/encode-run-state.md +1 -1
  9. package/docs/playback-start-forecast.md +113 -98
  10. package/package.json +51 -50
  11. package/routes/api/sources/files/get.js +106 -106
  12. package/scripts/build-audio-samples.mjs +31 -31
  13. package/services/encode/EncodeRun.js +67 -15
  14. package/services/encode/EncodeRuns.js +19 -7
  15. package/services/encode/OutputOpening.js +784 -784
  16. package/services/encode/audio-calibration.js +67 -67
  17. package/services/encode/audio-work-rate.js +15 -15
  18. package/services/encode/encode-exit.js +2 -0
  19. package/services/encode/hwaccel.js +1894 -1894
  20. package/services/encode/output/index.js +24 -24
  21. package/services/encode/piece-completeness.js +60 -2
  22. package/services/encode/run-command.js +757 -755
  23. package/services/encode/segment-formats/index.js +1 -0
  24. package/services/encode/segment-formats/mp4-boxes.js +8 -1
  25. package/services/media/ContainerOrchestrator.js +202 -202
  26. package/services/media/KeyframeTables.js +356 -356
  27. package/services/media/SubtitleCues.js +592 -592
  28. package/services/media/audio-inventory.js +299 -299
  29. package/services/media/container/AviContainer.js +268 -268
  30. package/services/media/container/Container.js +632 -632
  31. package/services/media/container/ContainerFactory.js +38 -38
  32. package/services/media/container/MatroskaContainer.js +1031 -1031
  33. package/services/media/container/Mp4Container.js +1354 -1354
  34. package/services/media/container/SubtitleFileContainer.js +339 -339
  35. package/services/media/container/ebml-stream.js +174 -174
  36. package/services/media/container/matroska-clusters.js +1165 -1165
  37. package/services/media/container/unavailable.js +77 -77
  38. package/services/media/tracks/ContainerTrack.js +84 -84
  39. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  40. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  41. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  42. package/services/torrent/Contents.js +382 -382
  43. package/services/torrent/episode-naming.js +182 -182
  44. package/services/torrent/torrent-pool.js +2688 -2688
  45. package/services/torrent/worker/client.js +836 -836
  46. package/services/torrent/worker/held-bytes.js +231 -231
  47. package/services/torrent/worker/worker.js +1137 -1137
  48. package/services/transport/data-channel-handler.js +1373 -1373
  49. package/services/viewer/PriorityMap.js +421 -421
  50. package/test/audio-calibration.test.js +23 -23
  51. package/test/auto-quality-step.test.js +762 -762
  52. package/test/coalesce.test.js +39 -39
  53. package/test/coverage-follows-the-disk.test.js +5 -0
  54. package/test/decode-cost.test.js +475 -475
  55. package/test/encode-run.test.js +58 -0
  56. package/test/episode-naming.test.js +131 -131
  57. package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
  58. package/test/fixtures/audio-frame-boundary.json +1 -1
  59. package/test/fixtures/refused-audio-20261002.json +10 -0
  60. package/test/fmp4-presentation-clock.test.js +4 -2
  61. package/test/format-keyed-outputs.test.js +78 -78
  62. package/test/health-metrics.test.js +11 -13
  63. package/test/held-read-ownership.test.js +77 -77
  64. package/test/helpers/encode-run.js +14 -6
  65. package/test/helpers/held-file.js +77 -77
  66. package/test/helpers/manager.js +62 -62
  67. package/test/helpers/matroska-file.js +296 -296
  68. package/test/keyframe-table-budget.test.js +123 -123
  69. package/test/matroska-cluster-discovery.test.js +323 -323
  70. package/test/one-output-one-session.test.js +273 -273
  71. package/test/output-speed.test.js +6 -1
  72. package/test/piece-completeness.test.js +37 -1
  73. package/test/quality-variants.test.js +1274 -1270
  74. package/test/rate-control-in-the-output.test.js +4 -3
  75. package/test/reads-not-yet-arrived.test.js +218 -218
  76. package/test/seek-frees-the-viewers-requests.test.js +1 -1
  77. package/test/segment-closed-on-both-branches.test.js +114 -114
  78. package/test/segment-judged-by-its-own-header.test.js +222 -222
  79. package/test/segment-serve-wiring.test.js +481 -481
  80. package/test/serving-existing-output.test.js +111 -111
  81. package/test/source-files-route.test.js +113 -113
  82. package/test/subtitle-cue-walk.test.js +385 -385
  83. package/test/subtitle-metadata.test.js +15 -15
  84. package/test/subtitle-plan-waits.test.js +125 -125
  85. package/test/subtitle-track-numbering.test.js +391 -391
  86. package/test/two-viewers-one-picture.test.js +508 -508
  87. package/test/viewer-requests-admission.test.js +92 -92
  88. package/test/viewer-starts-on-map.test.js +27 -27
  89. package/test/viewer-states.test.js +5 -2
  90. package/test/viewing-generations.test.js +390 -390
  91. package/test/whole-file-held-read.test.js +30 -30
  92. package/utils/coalesce.js +41 -41
@@ -1,77 +1,77 @@
1
- /**
2
- * @file Bytes that are not here yet, said as such.
3
- *
4
- * A container reads a file it does not hold: the bytes arrive off a swarm, in
5
- * whatever order the swarm delivers them. So every read has three outcomes —
6
- * the bytes, a statement the bytes make ("there is no Cues element"), and "the
7
- * bytes are not here yet" — and only the first two are facts about the file.
8
- *
9
- * Until 2026-10-01 the third came back as `null`, the same value the readers
10
- * return for "the element is absent". Every container then read a read that
11
- * had not arrived as an element that does not exist, and everything that
12
- * remembered a container's answer remembered that as a fact for the life of the
13
- * process: an embedded subtitle track planned with no clusters while its Cues
14
- * table was still downloading showed nothing for a whole session
15
- * (`research/subtitles-never-appear-2026-10-01.md`). The same shape was in the
16
- * container choice, the track table, the keyframe table, MP4's `moov` and AVI's
17
- * `idx1`.
18
- *
19
- * So a read that cannot be answered now THROWS this, and the rule for anything
20
- * that keeps an answer is one line: a value or a proven absence may be kept;
21
- * this may not.
22
- */
23
-
24
- /** Thrown where the bytes a reading needs have not arrived. */
25
- export class BytesUnavailable extends Error {
26
- /**
27
- * @param {number} start
28
- * @param {number} end - Inclusive.
29
- * @param {number} received - How many bytes the read did return.
30
- */
31
- constructor(start, end, received) {
32
- super(`bytes ${start}-${end} of the file are not available yet (${received} of ${end - start + 1} received)`);
33
- this.name = "BytesUnavailable";
34
- this.start = start;
35
- this.end = end;
36
- this.received = received;
37
- }
38
- }
39
-
40
- /**
41
- * Whether an error says "not here yet" rather than "something is wrong".
42
- *
43
- * @param {unknown} error
44
- * @returns {boolean}
45
- */
46
- export function isUnavailable(error) {
47
- return error instanceof BytesUnavailable || /** @type {any} */ (error)?.name === "BytesUnavailable";
48
- }
49
-
50
- /**
51
- * A reader that answers with every byte asked for, or throws.
52
- *
53
- * The range is clamped to the file first, so a reading that asks past the end
54
- * of a short file — a head window over a file smaller than the window — is
55
- * answered with what the file has, and that is complete. Anything shorter than
56
- * the clamped range is a read that did not finish: a stream that ended early,
57
- * a piece that is not downloaded, a store that could not serve it.
58
- *
59
- * @param {(start: number, end: number) => Promise<Buffer | null>} read
60
- * @param {number} fileSize
61
- * @returns {(start: number, end: number) => Promise<Buffer>}
62
- */
63
- export function strictReader(read, fileSize) {
64
- return async (start, end) => {
65
- const last = Math.min(end, fileSize - 1);
66
- if (!(start >= 0) || last < start) {
67
- return Buffer.alloc(0);
68
- }
69
- const bytes = await read(start, last);
70
- const wanted = last - start + 1;
71
- const received = bytes ? bytes.length : 0;
72
- if (received < wanted) {
73
- throw new BytesUnavailable(start, last, received);
74
- }
75
- return received === wanted ? bytes : bytes.subarray(0, wanted);
76
- };
77
- }
1
+ /**
2
+ * @file Bytes that are not here yet, said as such.
3
+ *
4
+ * A container reads a file it does not hold: the bytes arrive off a swarm, in
5
+ * whatever order the swarm delivers them. So every read has three outcomes —
6
+ * the bytes, a statement the bytes make ("there is no Cues element"), and "the
7
+ * bytes are not here yet" — and only the first two are facts about the file.
8
+ *
9
+ * Until 2026-10-01 the third came back as `null`, the same value the readers
10
+ * return for "the element is absent". Every container then read a read that
11
+ * had not arrived as an element that does not exist, and everything that
12
+ * remembered a container's answer remembered that as a fact for the life of the
13
+ * process: an embedded subtitle track planned with no clusters while its Cues
14
+ * table was still downloading showed nothing for a whole session
15
+ * (`research/subtitles-never-appear-2026-10-01.md`). The same shape was in the
16
+ * container choice, the track table, the keyframe table, MP4's `moov` and AVI's
17
+ * `idx1`.
18
+ *
19
+ * So a read that cannot be answered now THROWS this, and the rule for anything
20
+ * that keeps an answer is one line: a value or a proven absence may be kept;
21
+ * this may not.
22
+ */
23
+
24
+ /** Thrown where the bytes a reading needs have not arrived. */
25
+ export class BytesUnavailable extends Error {
26
+ /**
27
+ * @param {number} start
28
+ * @param {number} end - Inclusive.
29
+ * @param {number} received - How many bytes the read did return.
30
+ */
31
+ constructor(start, end, received) {
32
+ super(`bytes ${start}-${end} of the file are not available yet (${received} of ${end - start + 1} received)`);
33
+ this.name = "BytesUnavailable";
34
+ this.start = start;
35
+ this.end = end;
36
+ this.received = received;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Whether an error says "not here yet" rather than "something is wrong".
42
+ *
43
+ * @param {unknown} error
44
+ * @returns {boolean}
45
+ */
46
+ export function isUnavailable(error) {
47
+ return error instanceof BytesUnavailable || /** @type {any} */ (error)?.name === "BytesUnavailable";
48
+ }
49
+
50
+ /**
51
+ * A reader that answers with every byte asked for, or throws.
52
+ *
53
+ * The range is clamped to the file first, so a reading that asks past the end
54
+ * of a short file — a head window over a file smaller than the window — is
55
+ * answered with what the file has, and that is complete. Anything shorter than
56
+ * the clamped range is a read that did not finish: a stream that ended early,
57
+ * a piece that is not downloaded, a store that could not serve it.
58
+ *
59
+ * @param {(start: number, end: number) => Promise<Buffer | null>} read
60
+ * @param {number} fileSize
61
+ * @returns {(start: number, end: number) => Promise<Buffer>}
62
+ */
63
+ export function strictReader(read, fileSize) {
64
+ return async (start, end) => {
65
+ const last = Math.min(end, fileSize - 1);
66
+ if (!(start >= 0) || last < start) {
67
+ return Buffer.alloc(0);
68
+ }
69
+ const bytes = await read(start, last);
70
+ const wanted = last - start + 1;
71
+ const received = bytes ? bytes.length : 0;
72
+ if (received < wanted) {
73
+ throw new BytesUnavailable(start, last, received);
74
+ }
75
+ return received === wanted ? bytes : bytes.subarray(0, wanted);
76
+ };
77
+ }
@@ -1,84 +1,84 @@
1
- /**
2
- * @file Base container track — what the container declares about one track.
3
- *
4
- * Spec sources:
5
- * - Matroska RFC 9559 §5.1.4.1 TrackEntry: TrackNumber(0xD7), TrackType(0x83),
6
- * FlagEnabled(0xB9, default 1), FlagDefault(0x88, default 1),
7
- * Language(0x22B59C, default "eng"), LanguageBCP47(0x22B59D, MUST ignore
8
- * Language when present), Name(0x536E), CodecID(0x86), CodecPrivate(0x63A2).
9
- * - MP4 ISO/IEC 14496-12 §8.3.2 tkhd (track_ID, flags track_enabled 0x000001,
10
- * alternate_group), §8.4.2 mdhd (language, timescale), §8.4.5 hdlr
11
- * (handler_type), §8.4.6 elng (extended language).
12
- *
13
- * Only fields that exist for EVERY track type live here. Type-specific flags
14
- * (FlagForced, FlagHearingImpaired, FlagVisualImpaired, FlagOriginal,
15
- * FlagCommentary) belong to subclasses — RFC 9559 states FlagForced "Applies
16
- * only to subtitles", so VideoTrack must not carry it.
17
- */
18
-
19
- export class ContainerTrack {
20
- /**
21
- * @param {object} params
22
- * @param {number} params.trackNumber - Matroska TrackNumber or MP4 track_ID.
23
- * @param {number} params.declaredIndex - Position among ALL subtitle/audio/video tracks as container orders them; equals ffmpeg 0:s:N / 0:a:N for that type. Stable across image/text filtering.
24
- * @param {string} params.type - "video" | "audio" | "subtitle" | "other"
25
- * @param {string} params.codecId - Matroska CodecID or MP4 sample entry type (e.g. "S_TEXT/UTF8", "avc1", "mp4a").
26
- * @param {string} params.language - Three-letter code or packed mdhd code; empty when absent. For Matroska, when LanguageBCP47 is present this is the BCP47 value ignored per MUST — caller stores both, but `language` here is the resolved one.
27
- * @param {string} params.languageBcp47 - RFC 5646 tag from LanguageBCP47 / elng, or "".
28
- * @param {boolean} params.declaresLanguage - Whether the file explicitly supplies a language rather than relying on a container default.
29
- * @param {string} params.name - Track Name / title, or "".
30
- * @param {boolean} params.isEnabled - FlagEnabled / tkhd track_enabled. Default true per both specs. Matroska zero-length element means default, not disabled.
31
- * @param {boolean} params.isDefault - FlagDefault / tkhd? For MP4, derived from handler default? For Matroska, after applying default 1.
32
- * @param {boolean} params.declaresDefault - Whether FlagDefault was explicitly written (Matroska) or inferred. Needed because ffmpeg banner cannot distinguish "every track marked" from "no track marked".
33
- * @param {string} [params.codecPrivateB64] - Base64 of CodecPrivate (ASS header etc.), or "".
34
- * @param {number} [params.alternateGroup] - MP4 alternate_group (0 = no group).
35
- */
36
- constructor({
37
- trackNumber,
38
- declaredIndex,
39
- type,
40
- codecId,
41
- language,
42
- languageBcp47,
43
- declaresLanguage,
44
- languageSource = "",
45
- languageCode = "",
46
- name,
47
- isEnabled,
48
- isDefault,
49
- declaresDefault,
50
- codecPrivateB64 = "",
51
- alternateGroup = 0
52
- }) {
53
- this.trackNumber = trackNumber;
54
- this.declaredIndex = declaredIndex;
55
- this.type = type;
56
- this.codecId = codecId ?? "";
57
- this.language = language ?? "";
58
- this.languageBcp47 = languageBcp47 ?? "";
59
- this.declaresLanguage = declaresLanguage === true;
60
- // Where `language` came from: "bcp47", "language", "default" (the format's
61
- // own default, RFC 8794 §11.1.19), or "" where the container has no say.
62
- this.languageSource = typeof languageSource === "string" ? languageSource : "";
63
- // The three-letter form the file states, or its default — what ffmpeg's
64
- // banner prints, and so what a list lined up against it has to carry even
65
- // where an RFC 5646 tag is the track's language.
66
- this.languageCode = typeof languageCode === "string" ? languageCode : "";
67
- this.name = name ?? "";
68
- this.isEnabled = isEnabled !== false;
69
- this.isDefault = isDefault === true;
70
- this.declaresDefault = declaresDefault === true;
71
- this.codecPrivateB64 = codecPrivateB64 ?? "";
72
- this.alternateGroup = Number.isFinite(alternateGroup) ? alternateGroup : 0;
73
- }
74
-
75
- /** Whether this track should be offered in UI menus. Base rule: disabled tracks hidden but still counted for declaredIndex. */
76
- isOfferable() {
77
- return this.isEnabled;
78
- }
79
-
80
- /** RFC 5646 tag wins over three-letter code when present. */
81
- resolvedLanguage() {
82
- return this.languageBcp47 || this.language;
83
- }
84
- }
1
+ /**
2
+ * @file Base container track — what the container declares about one track.
3
+ *
4
+ * Spec sources:
5
+ * - Matroska RFC 9559 §5.1.4.1 TrackEntry: TrackNumber(0xD7), TrackType(0x83),
6
+ * FlagEnabled(0xB9, default 1), FlagDefault(0x88, default 1),
7
+ * Language(0x22B59C, default "eng"), LanguageBCP47(0x22B59D, MUST ignore
8
+ * Language when present), Name(0x536E), CodecID(0x86), CodecPrivate(0x63A2).
9
+ * - MP4 ISO/IEC 14496-12 §8.3.2 tkhd (track_ID, flags track_enabled 0x000001,
10
+ * alternate_group), §8.4.2 mdhd (language, timescale), §8.4.5 hdlr
11
+ * (handler_type), §8.4.6 elng (extended language).
12
+ *
13
+ * Only fields that exist for EVERY track type live here. Type-specific flags
14
+ * (FlagForced, FlagHearingImpaired, FlagVisualImpaired, FlagOriginal,
15
+ * FlagCommentary) belong to subclasses — RFC 9559 states FlagForced "Applies
16
+ * only to subtitles", so VideoTrack must not carry it.
17
+ */
18
+
19
+ export class ContainerTrack {
20
+ /**
21
+ * @param {object} params
22
+ * @param {number} params.trackNumber - Matroska TrackNumber or MP4 track_ID.
23
+ * @param {number} params.declaredIndex - Position among ALL subtitle/audio/video tracks as container orders them; equals ffmpeg 0:s:N / 0:a:N for that type. Stable across image/text filtering.
24
+ * @param {string} params.type - "video" | "audio" | "subtitle" | "other"
25
+ * @param {string} params.codecId - Matroska CodecID or MP4 sample entry type (e.g. "S_TEXT/UTF8", "avc1", "mp4a").
26
+ * @param {string} params.language - Three-letter code or packed mdhd code; empty when absent. For Matroska, when LanguageBCP47 is present this is the BCP47 value ignored per MUST — caller stores both, but `language` here is the resolved one.
27
+ * @param {string} params.languageBcp47 - RFC 5646 tag from LanguageBCP47 / elng, or "".
28
+ * @param {boolean} params.declaresLanguage - Whether the file explicitly supplies a language rather than relying on a container default.
29
+ * @param {string} params.name - Track Name / title, or "".
30
+ * @param {boolean} params.isEnabled - FlagEnabled / tkhd track_enabled. Default true per both specs. Matroska zero-length element means default, not disabled.
31
+ * @param {boolean} params.isDefault - FlagDefault / tkhd? For MP4, derived from handler default? For Matroska, after applying default 1.
32
+ * @param {boolean} params.declaresDefault - Whether FlagDefault was explicitly written (Matroska) or inferred. Needed because ffmpeg banner cannot distinguish "every track marked" from "no track marked".
33
+ * @param {string} [params.codecPrivateB64] - Base64 of CodecPrivate (ASS header etc.), or "".
34
+ * @param {number} [params.alternateGroup] - MP4 alternate_group (0 = no group).
35
+ */
36
+ constructor({
37
+ trackNumber,
38
+ declaredIndex,
39
+ type,
40
+ codecId,
41
+ language,
42
+ languageBcp47,
43
+ declaresLanguage,
44
+ languageSource = "",
45
+ languageCode = "",
46
+ name,
47
+ isEnabled,
48
+ isDefault,
49
+ declaresDefault,
50
+ codecPrivateB64 = "",
51
+ alternateGroup = 0
52
+ }) {
53
+ this.trackNumber = trackNumber;
54
+ this.declaredIndex = declaredIndex;
55
+ this.type = type;
56
+ this.codecId = codecId ?? "";
57
+ this.language = language ?? "";
58
+ this.languageBcp47 = languageBcp47 ?? "";
59
+ this.declaresLanguage = declaresLanguage === true;
60
+ // Where `language` came from: "bcp47", "language", "default" (the format's
61
+ // own default, RFC 8794 §11.1.19), or "" where the container has no say.
62
+ this.languageSource = typeof languageSource === "string" ? languageSource : "";
63
+ // The three-letter form the file states, or its default — what ffmpeg's
64
+ // banner prints, and so what a list lined up against it has to carry even
65
+ // where an RFC 5646 tag is the track's language.
66
+ this.languageCode = typeof languageCode === "string" ? languageCode : "";
67
+ this.name = name ?? "";
68
+ this.isEnabled = isEnabled !== false;
69
+ this.isDefault = isDefault === true;
70
+ this.declaresDefault = declaresDefault === true;
71
+ this.codecPrivateB64 = codecPrivateB64 ?? "";
72
+ this.alternateGroup = Number.isFinite(alternateGroup) ? alternateGroup : 0;
73
+ }
74
+
75
+ /** Whether this track should be offered in UI menus. Base rule: disabled tracks hidden but still counted for declaredIndex. */
76
+ isOfferable() {
77
+ return this.isEnabled;
78
+ }
79
+
80
+ /** RFC 5646 tag wins over three-letter code when present. */
81
+ resolvedLanguage() {
82
+ return this.languageBcp47 || this.language;
83
+ }
84
+ }