@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,296 +1,296 @@
1
- /**
2
- * @file Synthetic Matroska files for the checks — built element by element,
3
- * so every check states exactly what the file says and where.
4
- *
5
- * Positions inside the Segment are known only after the elements before them
6
- * are built, so a file is described as a list of top-level elements, and the
7
- * SeekHead and Cues that point at others are built from the lengths of the
8
- * rest (all positions are fixed-width, so they do not change the lengths they
9
- * are computed from).
10
- */
11
-
12
- import { crc32 } from "node:zlib";
13
-
14
- export const ID = {
15
- EBML: 0x1a45dfa3,
16
- SEGMENT: 0x18538067,
17
- SEEK_HEAD: 0x114d9b74,
18
- SEEK: 0x4dbb,
19
- SEEK_ID: 0x53ab,
20
- SEEK_POSITION: 0x53ac,
21
- INFO: 0x1549a966,
22
- TIMESTAMP_SCALE: 0x2ad7b1,
23
- TRACKS: 0x1654ae6b,
24
- TRACK_ENTRY: 0xae,
25
- TRACK_NUMBER: 0xd7,
26
- TRACK_TYPE: 0x83,
27
- CODEC_ID: 0x86,
28
- LANGUAGE: 0x22b59c,
29
- LANGUAGE_BCP47: 0x22b59d,
30
- NAME: 0x536e,
31
- CUES: 0x1c53bb6b,
32
- CUE_POINT: 0xbb,
33
- CUE_TIME: 0xb3,
34
- CUE_TRACK_POSITIONS: 0xb7,
35
- CUE_TRACK: 0xf7,
36
- CUE_CLUSTER_POSITION: 0xf1,
37
- CLUSTER: 0x1f43b675,
38
- TIMESTAMP: 0xe7,
39
- CRC32: 0xbf,
40
- SIMPLE_BLOCK: 0xa3,
41
- BLOCK_GROUP: 0xa0,
42
- BLOCK: 0xa1,
43
- BLOCK_DURATION: 0x9b,
44
- VOID: 0xec
45
- };
46
-
47
- /** @param {number} id @returns {Buffer} */
48
- export function idBytes(id) {
49
- const bytes = [];
50
- let rest = id;
51
- while (rest > 0) {
52
- bytes.unshift(rest & 0xff);
53
- rest = Math.floor(rest / 256);
54
- }
55
- return Buffer.from(bytes);
56
- }
57
-
58
- /** A four-byte size: a valid vint whatever the payload. @param {number} size */
59
- function sizeBytes(size) {
60
- const buffer = Buffer.alloc(4);
61
- buffer.writeUInt32BE(size, 0);
62
- buffer[0] |= 0x10;
63
- return buffer;
64
- }
65
-
66
- /** @param {number} id @param {Buffer} payload @returns {Buffer} */
67
- export function element(id, payload) {
68
- return Buffer.concat([idBytes(id), sizeBytes(payload.length), payload]);
69
- }
70
-
71
- /** An element whose size field says "unknown" (one byte, all value bits set). */
72
- export function unknownSizeElement(id, payload) {
73
- return Buffer.concat([idBytes(id), Buffer.from([0xff]), payload]);
74
- }
75
-
76
- /** @param {number} id @param {number} value */
77
- export function uintElement(id, value) {
78
- const bytes = [];
79
- let rest = value;
80
- do {
81
- bytes.unshift(rest & 0xff);
82
- rest = Math.floor(rest / 256);
83
- } while (rest > 0);
84
- return element(id, Buffer.from(bytes));
85
- }
86
-
87
- /** A fixed-width unsigned integer, so a position computed twice keeps its length. */
88
- export function uint32Element(id, value) {
89
- const payload = Buffer.alloc(4);
90
- payload.writeUInt32BE(value, 0);
91
- return element(id, payload);
92
- }
93
-
94
- /** @param {number} id @param {string} value */
95
- export function stringElement(id, value) {
96
- return element(id, Buffer.from(value, "utf8"));
97
- }
98
-
99
- /**
100
- * @param {{ number: number, type: number, codecId: string, language?: string | null, languageBcp47?: string, name?: string }} track
101
- * @returns {Buffer}
102
- */
103
- export function trackEntry({ number, type, codecId, language = null, languageBcp47 = "", name = "" }) {
104
- const parts = [uintElement(ID.TRACK_NUMBER, number), uintElement(ID.TRACK_TYPE, type), stringElement(ID.CODEC_ID, codecId)];
105
- if (language !== null) {
106
- parts.push(stringElement(ID.LANGUAGE, language));
107
- }
108
- if (languageBcp47) {
109
- parts.push(stringElement(ID.LANGUAGE_BCP47, languageBcp47));
110
- }
111
- if (name) {
112
- parts.push(stringElement(ID.NAME, name));
113
- }
114
- return element(ID.TRACK_ENTRY, Buffer.concat(parts));
115
- }
116
-
117
- /**
118
- * One cue, as a BlockGroup carrying its own duration.
119
- *
120
- * @param {{ track: number, relativeTicks: number, durationTicks: number, text: string }} params
121
- */
122
- export function cueBlock({ track, relativeTicks, durationTicks, text }) {
123
- const header = Buffer.alloc(4);
124
- header[0] = 0x80 | track;
125
- header.writeInt16BE(relativeTicks, 1);
126
- header[3] = 0;
127
- const block = element(ID.BLOCK, Buffer.concat([header, Buffer.from(text, "utf8")]));
128
- return element(ID.BLOCK_GROUP, Buffer.concat([block, uintElement(ID.BLOCK_DURATION, durationTicks)]));
129
- }
130
-
131
- /**
132
- * A picture block: a SimpleBlock of `payload` bytes on `track`.
133
- *
134
- * @param {{ track: number, payload: Buffer }} params
135
- */
136
- export function pictureBlock({ track, payload }) {
137
- const header = Buffer.alloc(4);
138
- header[0] = 0x80 | track;
139
- header.writeInt16BE(0, 1);
140
- header[3] = 0x80;
141
- return element(ID.SIMPLE_BLOCK, Buffer.concat([header, payload]));
142
- }
143
-
144
- /**
145
- * A cluster's DATA: its Timestamp (optionally after a CRC-32) and its blocks.
146
- *
147
- * @param {{ ticks: number, blocks: Buffer[], crc?: boolean | "wrong" }} params
148
- * @returns {Buffer}
149
- */
150
- export function clusterData({ ticks, blocks, crc = false }) {
151
- const rest = Buffer.concat([uintElement(ID.TIMESTAMP, ticks), ...blocks]);
152
- if (!crc) {
153
- return rest;
154
- }
155
- const value = Buffer.alloc(4);
156
- value.writeUInt32LE(crc === "wrong" ? (crc32(rest) ^ 0xffffffff) >>> 0 : crc32(rest), 0);
157
- return Buffer.concat([Buffer.concat([idBytes(ID.CRC32), Buffer.from([0x84]), value]), rest]);
158
- }
159
-
160
- /**
161
- * A whole file.
162
- *
163
- * `clusters` are each `{ data, unknownSize? }`. `cues` says which tracks the
164
- * Cues table names for every cluster: an array of track numbers, or null for
165
- * no Cues element at all. `cuesBeforeClusters` puts the Cues element ahead of
166
- * the clusters with no SeekHead entry for it.
167
- *
168
- * @param {object} params
169
- * @param {Buffer[]} params.tracks - TrackEntry elements.
170
- * @param {Array<{ data: Buffer, unknownSize?: boolean, ticks: number }>} params.clusters
171
- * @param {number[] | null} [params.cues]
172
- * @param {boolean} [params.cuesBeforeClusters]
173
- * @returns {{ file: Buffer, clusterAt: number[], cuesAt: number | null, segmentDataOffset: number }}
174
- */
175
- export function buildMatroska({ tracks, clusters, cues = [], cuesBeforeClusters = false }) {
176
- const info = element(ID.INFO, uintElement(ID.TIMESTAMP_SCALE, 1_000_000));
177
- const tracksElement = element(ID.TRACKS, Buffer.concat(tracks));
178
- const clusterElements = clusters.map((cluster) =>
179
- cluster.unknownSize ? unknownSizeElement(ID.CLUSTER, cluster.data) : element(ID.CLUSTER, cluster.data)
180
- );
181
-
182
- const cuesWith = (positions) =>
183
- cues === null
184
- ? Buffer.alloc(0)
185
- : element(
186
- ID.CUES,
187
- Buffer.concat(
188
- clusters.map((cluster, index) =>
189
- element(
190
- ID.CUE_POINT,
191
- Buffer.concat([
192
- uint32Element(ID.CUE_TIME, cluster.ticks),
193
- ...cues.map((track) =>
194
- element(
195
- ID.CUE_TRACK_POSITIONS,
196
- Buffer.concat([uintElement(ID.CUE_TRACK, track), uint32Element(ID.CUE_CLUSTER_POSITION, positions[index])])
197
- )
198
- )
199
- ])
200
- )
201
- )
202
- )
203
- );
204
- const seekEntry = (targetId, position) =>
205
- element(ID.SEEK, Buffer.concat([element(ID.SEEK_ID, idBytes(targetId)), uint32Element(ID.SEEK_POSITION, position)]));
206
- const seekHeadWith = (infoAt, tracksAt, cuesAt) =>
207
- element(
208
- ID.SEEK_HEAD,
209
- Buffer.concat([
210
- seekEntry(ID.INFO, infoAt),
211
- seekEntry(ID.TRACKS, tracksAt),
212
- ...(cues !== null && !cuesBeforeClusters ? [seekEntry(ID.CUES, cuesAt)] : [])
213
- ])
214
- );
215
-
216
- const zeros = clusters.map(() => 0);
217
- const headLength = seekHeadWith(0, 0, 0).length;
218
- const infoAt = headLength;
219
- const tracksAt = infoAt + info.length;
220
- const cuesLength = cuesWith(zeros).length;
221
- const firstClusterAt = tracksAt + tracksElement.length + (cuesBeforeClusters ? cuesLength : 0);
222
- const positions = [];
223
- let at = firstClusterAt;
224
- for (const cluster of clusterElements) {
225
- positions.push(at);
226
- at += cluster.length;
227
- }
228
- const cuesAt = cues === null ? null : cuesBeforeClusters ? tracksAt + tracksElement.length : at;
229
- const parts = [seekHeadWith(infoAt, tracksAt, cuesAt ?? 0), info, tracksElement];
230
- if (cuesBeforeClusters) {
231
- parts.push(cuesWith(positions));
232
- }
233
- parts.push(...clusterElements);
234
- if (!cuesBeforeClusters) {
235
- parts.push(cuesWith(positions));
236
- }
237
- const payload = Buffer.concat(parts);
238
- const ebml = element(ID.EBML, Buffer.from([0x42, 0x86, 0x81, 0x01]));
239
- const segment = element(ID.SEGMENT, payload);
240
- const segmentDataOffset = ebml.length + segment.length - payload.length;
241
- return {
242
- file: Buffer.concat([ebml, segment]),
243
- clusterAt: positions.map((position) => segmentDataOffset + position),
244
- cuesAt: cuesAt === null ? null : segmentDataOffset + cuesAt,
245
- segmentDataOffset
246
- };
247
- }
248
-
249
- /**
250
- * A reader over a buffer that answers null for the ranges a check says are
251
- * not here, and records every range asked for.
252
- *
253
- * @param {Buffer} bytes
254
- * @param {{ missing?: (start: number, end: number) => boolean, short?: boolean }} [options]
255
- */
256
- export function readerOver(bytes, options = {}) {
257
- const reads = [];
258
- const read = async (start, end) => {
259
- reads.push({ start, end });
260
- if (options.missing?.(start, end)) {
261
- return null;
262
- }
263
- const slice = bytes.subarray(start, end + 1);
264
- return options.short ? slice.subarray(0, Math.max(0, slice.length - 1)) : Buffer.from(slice);
265
- };
266
- return { read, reads };
267
- }
268
-
269
- /**
270
- * What the cue walk may read: the held ranges, a strict read of them, a
271
- * portion and where viewers stand.
272
- *
273
- * @param {Buffer} bytes
274
- * @param {Array<[number, number]>} ranges
275
- * @param {{ portionBytes?: number, wantedSeconds?: number[], reads?: Array<{ start: number, end: number }> }} [options]
276
- * @returns {import("../../services/media/container/Container.js").HeldReader}
277
- */
278
- export function heldOver(bytes, ranges, options = {}) {
279
- const holds = (start, end) => ranges.some(([from, to]) => start >= from && end <= to);
280
- return {
281
- ranges,
282
- isHeld: (start, end) => holds(start, Math.min(end, bytes.length - 1)),
283
- read: async (start, end) => {
284
- options.reads?.push({ start, end });
285
- const last = Math.min(end, bytes.length - 1);
286
- if (!holds(start, last)) {
287
- const error = new Error(`bytes ${start}-${last} are not available yet`);
288
- error.name = "BytesUnavailable";
289
- throw error;
290
- }
291
- return Buffer.from(bytes.subarray(start, last + 1));
292
- },
293
- portionBytes: options.portionBytes ?? Number.POSITIVE_INFINITY,
294
- wantedSeconds: options.wantedSeconds ?? []
295
- };
296
- }
1
+ /**
2
+ * @file Synthetic Matroska files for the checks — built element by element,
3
+ * so every check states exactly what the file says and where.
4
+ *
5
+ * Positions inside the Segment are known only after the elements before them
6
+ * are built, so a file is described as a list of top-level elements, and the
7
+ * SeekHead and Cues that point at others are built from the lengths of the
8
+ * rest (all positions are fixed-width, so they do not change the lengths they
9
+ * are computed from).
10
+ */
11
+
12
+ import { crc32 } from "node:zlib";
13
+
14
+ export const ID = {
15
+ EBML: 0x1a45dfa3,
16
+ SEGMENT: 0x18538067,
17
+ SEEK_HEAD: 0x114d9b74,
18
+ SEEK: 0x4dbb,
19
+ SEEK_ID: 0x53ab,
20
+ SEEK_POSITION: 0x53ac,
21
+ INFO: 0x1549a966,
22
+ TIMESTAMP_SCALE: 0x2ad7b1,
23
+ TRACKS: 0x1654ae6b,
24
+ TRACK_ENTRY: 0xae,
25
+ TRACK_NUMBER: 0xd7,
26
+ TRACK_TYPE: 0x83,
27
+ CODEC_ID: 0x86,
28
+ LANGUAGE: 0x22b59c,
29
+ LANGUAGE_BCP47: 0x22b59d,
30
+ NAME: 0x536e,
31
+ CUES: 0x1c53bb6b,
32
+ CUE_POINT: 0xbb,
33
+ CUE_TIME: 0xb3,
34
+ CUE_TRACK_POSITIONS: 0xb7,
35
+ CUE_TRACK: 0xf7,
36
+ CUE_CLUSTER_POSITION: 0xf1,
37
+ CLUSTER: 0x1f43b675,
38
+ TIMESTAMP: 0xe7,
39
+ CRC32: 0xbf,
40
+ SIMPLE_BLOCK: 0xa3,
41
+ BLOCK_GROUP: 0xa0,
42
+ BLOCK: 0xa1,
43
+ BLOCK_DURATION: 0x9b,
44
+ VOID: 0xec
45
+ };
46
+
47
+ /** @param {number} id @returns {Buffer} */
48
+ export function idBytes(id) {
49
+ const bytes = [];
50
+ let rest = id;
51
+ while (rest > 0) {
52
+ bytes.unshift(rest & 0xff);
53
+ rest = Math.floor(rest / 256);
54
+ }
55
+ return Buffer.from(bytes);
56
+ }
57
+
58
+ /** A four-byte size: a valid vint whatever the payload. @param {number} size */
59
+ function sizeBytes(size) {
60
+ const buffer = Buffer.alloc(4);
61
+ buffer.writeUInt32BE(size, 0);
62
+ buffer[0] |= 0x10;
63
+ return buffer;
64
+ }
65
+
66
+ /** @param {number} id @param {Buffer} payload @returns {Buffer} */
67
+ export function element(id, payload) {
68
+ return Buffer.concat([idBytes(id), sizeBytes(payload.length), payload]);
69
+ }
70
+
71
+ /** An element whose size field says "unknown" (one byte, all value bits set). */
72
+ export function unknownSizeElement(id, payload) {
73
+ return Buffer.concat([idBytes(id), Buffer.from([0xff]), payload]);
74
+ }
75
+
76
+ /** @param {number} id @param {number} value */
77
+ export function uintElement(id, value) {
78
+ const bytes = [];
79
+ let rest = value;
80
+ do {
81
+ bytes.unshift(rest & 0xff);
82
+ rest = Math.floor(rest / 256);
83
+ } while (rest > 0);
84
+ return element(id, Buffer.from(bytes));
85
+ }
86
+
87
+ /** A fixed-width unsigned integer, so a position computed twice keeps its length. */
88
+ export function uint32Element(id, value) {
89
+ const payload = Buffer.alloc(4);
90
+ payload.writeUInt32BE(value, 0);
91
+ return element(id, payload);
92
+ }
93
+
94
+ /** @param {number} id @param {string} value */
95
+ export function stringElement(id, value) {
96
+ return element(id, Buffer.from(value, "utf8"));
97
+ }
98
+
99
+ /**
100
+ * @param {{ number: number, type: number, codecId: string, language?: string | null, languageBcp47?: string, name?: string }} track
101
+ * @returns {Buffer}
102
+ */
103
+ export function trackEntry({ number, type, codecId, language = null, languageBcp47 = "", name = "" }) {
104
+ const parts = [uintElement(ID.TRACK_NUMBER, number), uintElement(ID.TRACK_TYPE, type), stringElement(ID.CODEC_ID, codecId)];
105
+ if (language !== null) {
106
+ parts.push(stringElement(ID.LANGUAGE, language));
107
+ }
108
+ if (languageBcp47) {
109
+ parts.push(stringElement(ID.LANGUAGE_BCP47, languageBcp47));
110
+ }
111
+ if (name) {
112
+ parts.push(stringElement(ID.NAME, name));
113
+ }
114
+ return element(ID.TRACK_ENTRY, Buffer.concat(parts));
115
+ }
116
+
117
+ /**
118
+ * One cue, as a BlockGroup carrying its own duration.
119
+ *
120
+ * @param {{ track: number, relativeTicks: number, durationTicks: number, text: string }} params
121
+ */
122
+ export function cueBlock({ track, relativeTicks, durationTicks, text }) {
123
+ const header = Buffer.alloc(4);
124
+ header[0] = 0x80 | track;
125
+ header.writeInt16BE(relativeTicks, 1);
126
+ header[3] = 0;
127
+ const block = element(ID.BLOCK, Buffer.concat([header, Buffer.from(text, "utf8")]));
128
+ return element(ID.BLOCK_GROUP, Buffer.concat([block, uintElement(ID.BLOCK_DURATION, durationTicks)]));
129
+ }
130
+
131
+ /**
132
+ * A picture block: a SimpleBlock of `payload` bytes on `track`.
133
+ *
134
+ * @param {{ track: number, payload: Buffer }} params
135
+ */
136
+ export function pictureBlock({ track, payload }) {
137
+ const header = Buffer.alloc(4);
138
+ header[0] = 0x80 | track;
139
+ header.writeInt16BE(0, 1);
140
+ header[3] = 0x80;
141
+ return element(ID.SIMPLE_BLOCK, Buffer.concat([header, payload]));
142
+ }
143
+
144
+ /**
145
+ * A cluster's DATA: its Timestamp (optionally after a CRC-32) and its blocks.
146
+ *
147
+ * @param {{ ticks: number, blocks: Buffer[], crc?: boolean | "wrong" }} params
148
+ * @returns {Buffer}
149
+ */
150
+ export function clusterData({ ticks, blocks, crc = false }) {
151
+ const rest = Buffer.concat([uintElement(ID.TIMESTAMP, ticks), ...blocks]);
152
+ if (!crc) {
153
+ return rest;
154
+ }
155
+ const value = Buffer.alloc(4);
156
+ value.writeUInt32LE(crc === "wrong" ? (crc32(rest) ^ 0xffffffff) >>> 0 : crc32(rest), 0);
157
+ return Buffer.concat([Buffer.concat([idBytes(ID.CRC32), Buffer.from([0x84]), value]), rest]);
158
+ }
159
+
160
+ /**
161
+ * A whole file.
162
+ *
163
+ * `clusters` are each `{ data, unknownSize? }`. `cues` says which tracks the
164
+ * Cues table names for every cluster: an array of track numbers, or null for
165
+ * no Cues element at all. `cuesBeforeClusters` puts the Cues element ahead of
166
+ * the clusters with no SeekHead entry for it.
167
+ *
168
+ * @param {object} params
169
+ * @param {Buffer[]} params.tracks - TrackEntry elements.
170
+ * @param {Array<{ data: Buffer, unknownSize?: boolean, ticks: number }>} params.clusters
171
+ * @param {number[] | null} [params.cues]
172
+ * @param {boolean} [params.cuesBeforeClusters]
173
+ * @returns {{ file: Buffer, clusterAt: number[], cuesAt: number | null, segmentDataOffset: number }}
174
+ */
175
+ export function buildMatroska({ tracks, clusters, cues = [], cuesBeforeClusters = false }) {
176
+ const info = element(ID.INFO, uintElement(ID.TIMESTAMP_SCALE, 1_000_000));
177
+ const tracksElement = element(ID.TRACKS, Buffer.concat(tracks));
178
+ const clusterElements = clusters.map((cluster) =>
179
+ cluster.unknownSize ? unknownSizeElement(ID.CLUSTER, cluster.data) : element(ID.CLUSTER, cluster.data)
180
+ );
181
+
182
+ const cuesWith = (positions) =>
183
+ cues === null
184
+ ? Buffer.alloc(0)
185
+ : element(
186
+ ID.CUES,
187
+ Buffer.concat(
188
+ clusters.map((cluster, index) =>
189
+ element(
190
+ ID.CUE_POINT,
191
+ Buffer.concat([
192
+ uint32Element(ID.CUE_TIME, cluster.ticks),
193
+ ...cues.map((track) =>
194
+ element(
195
+ ID.CUE_TRACK_POSITIONS,
196
+ Buffer.concat([uintElement(ID.CUE_TRACK, track), uint32Element(ID.CUE_CLUSTER_POSITION, positions[index])])
197
+ )
198
+ )
199
+ ])
200
+ )
201
+ )
202
+ )
203
+ );
204
+ const seekEntry = (targetId, position) =>
205
+ element(ID.SEEK, Buffer.concat([element(ID.SEEK_ID, idBytes(targetId)), uint32Element(ID.SEEK_POSITION, position)]));
206
+ const seekHeadWith = (infoAt, tracksAt, cuesAt) =>
207
+ element(
208
+ ID.SEEK_HEAD,
209
+ Buffer.concat([
210
+ seekEntry(ID.INFO, infoAt),
211
+ seekEntry(ID.TRACKS, tracksAt),
212
+ ...(cues !== null && !cuesBeforeClusters ? [seekEntry(ID.CUES, cuesAt)] : [])
213
+ ])
214
+ );
215
+
216
+ const zeros = clusters.map(() => 0);
217
+ const headLength = seekHeadWith(0, 0, 0).length;
218
+ const infoAt = headLength;
219
+ const tracksAt = infoAt + info.length;
220
+ const cuesLength = cuesWith(zeros).length;
221
+ const firstClusterAt = tracksAt + tracksElement.length + (cuesBeforeClusters ? cuesLength : 0);
222
+ const positions = [];
223
+ let at = firstClusterAt;
224
+ for (const cluster of clusterElements) {
225
+ positions.push(at);
226
+ at += cluster.length;
227
+ }
228
+ const cuesAt = cues === null ? null : cuesBeforeClusters ? tracksAt + tracksElement.length : at;
229
+ const parts = [seekHeadWith(infoAt, tracksAt, cuesAt ?? 0), info, tracksElement];
230
+ if (cuesBeforeClusters) {
231
+ parts.push(cuesWith(positions));
232
+ }
233
+ parts.push(...clusterElements);
234
+ if (!cuesBeforeClusters) {
235
+ parts.push(cuesWith(positions));
236
+ }
237
+ const payload = Buffer.concat(parts);
238
+ const ebml = element(ID.EBML, Buffer.from([0x42, 0x86, 0x81, 0x01]));
239
+ const segment = element(ID.SEGMENT, payload);
240
+ const segmentDataOffset = ebml.length + segment.length - payload.length;
241
+ return {
242
+ file: Buffer.concat([ebml, segment]),
243
+ clusterAt: positions.map((position) => segmentDataOffset + position),
244
+ cuesAt: cuesAt === null ? null : segmentDataOffset + cuesAt,
245
+ segmentDataOffset
246
+ };
247
+ }
248
+
249
+ /**
250
+ * A reader over a buffer that answers null for the ranges a check says are
251
+ * not here, and records every range asked for.
252
+ *
253
+ * @param {Buffer} bytes
254
+ * @param {{ missing?: (start: number, end: number) => boolean, short?: boolean }} [options]
255
+ */
256
+ export function readerOver(bytes, options = {}) {
257
+ const reads = [];
258
+ const read = async (start, end) => {
259
+ reads.push({ start, end });
260
+ if (options.missing?.(start, end)) {
261
+ return null;
262
+ }
263
+ const slice = bytes.subarray(start, end + 1);
264
+ return options.short ? slice.subarray(0, Math.max(0, slice.length - 1)) : Buffer.from(slice);
265
+ };
266
+ return { read, reads };
267
+ }
268
+
269
+ /**
270
+ * What the cue walk may read: the held ranges, a strict read of them, a
271
+ * portion and where viewers stand.
272
+ *
273
+ * @param {Buffer} bytes
274
+ * @param {Array<[number, number]>} ranges
275
+ * @param {{ portionBytes?: number, wantedSeconds?: number[], reads?: Array<{ start: number, end: number }> }} [options]
276
+ * @returns {import("../../services/media/container/Container.js").HeldReader}
277
+ */
278
+ export function heldOver(bytes, ranges, options = {}) {
279
+ const holds = (start, end) => ranges.some(([from, to]) => start >= from && end <= to);
280
+ return {
281
+ ranges,
282
+ isHeld: (start, end) => holds(start, Math.min(end, bytes.length - 1)),
283
+ read: async (start, end) => {
284
+ options.reads?.push({ start, end });
285
+ const last = Math.min(end, bytes.length - 1);
286
+ if (!holds(start, last)) {
287
+ const error = new Error(`bytes ${start}-${last} are not available yet`);
288
+ error.name = "BytesUnavailable";
289
+ throw error;
290
+ }
291
+ return Buffer.from(bytes.subarray(start, last + 1));
292
+ },
293
+ portionBytes: options.portionBytes ?? Number.POSITIVE_INFINITY,
294
+ wantedSeconds: options.wantedSeconds ?? []
295
+ };
296
+ }