@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.
- package/AGENTS.md +32 -12
- package/CHANGELOG.md +2340 -2330
- package/assets/calibration/audio/NOTICE.md +19 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/biome.json +412 -412
- package/docs/browser-proxy-contract.md +155 -155
- package/docs/container-architecture.md +273 -273
- package/docs/encode-run-state.md +1 -1
- package/docs/playback-start-forecast.md +113 -98
- package/package.json +51 -50
- package/routes/api/sources/files/get.js +106 -106
- package/scripts/build-audio-samples.mjs +31 -31
- package/services/encode/EncodeRun.js +67 -15
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/OutputOpening.js +784 -784
- package/services/encode/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- package/services/encode/encode-exit.js +2 -0
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/output/index.js +24 -24
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/run-command.js +757 -755
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/media/ContainerOrchestrator.js +202 -202
- package/services/media/KeyframeTables.js +356 -356
- package/services/media/SubtitleCues.js +592 -592
- package/services/media/audio-inventory.js +299 -299
- package/services/media/container/AviContainer.js +268 -268
- package/services/media/container/Container.js +632 -632
- package/services/media/container/ContainerFactory.js +38 -38
- package/services/media/container/MatroskaContainer.js +1031 -1031
- package/services/media/container/Mp4Container.js +1354 -1354
- package/services/media/container/SubtitleFileContainer.js +339 -339
- package/services/media/container/ebml-stream.js +174 -174
- package/services/media/container/matroska-clusters.js +1165 -1165
- package/services/media/container/unavailable.js +77 -77
- package/services/media/tracks/ContainerTrack.js +84 -84
- package/services/media/tracks/TextSubtitleTrack.js +295 -295
- package/services/storage/piece-store/shared-piece-store.js +1937 -1937
- package/services/storage/segment-store/SegmentStore.js +1294 -1294
- package/services/torrent/Contents.js +382 -382
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/torrent-pool.js +2688 -2688
- package/services/torrent/worker/client.js +836 -836
- package/services/torrent/worker/held-bytes.js +231 -231
- package/services/torrent/worker/worker.js +1137 -1137
- package/services/transport/data-channel-handler.js +1373 -1373
- package/services/viewer/PriorityMap.js +421 -421
- package/test/audio-calibration.test.js +23 -23
- package/test/auto-quality-step.test.js +762 -762
- package/test/coalesce.test.js +39 -39
- package/test/coverage-follows-the-disk.test.js +5 -0
- package/test/decode-cost.test.js +475 -475
- package/test/encode-run.test.js +58 -0
- package/test/episode-naming.test.js +131 -131
- package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
- package/test/fixtures/audio-frame-boundary.json +1 -1
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +11 -13
- package/test/held-read-ownership.test.js +77 -77
- package/test/helpers/encode-run.js +14 -6
- package/test/helpers/held-file.js +77 -77
- package/test/helpers/manager.js +62 -62
- package/test/helpers/matroska-file.js +296 -296
- package/test/keyframe-table-budget.test.js +123 -123
- package/test/matroska-cluster-discovery.test.js +323 -323
- package/test/one-output-one-session.test.js +273 -273
- package/test/output-speed.test.js +6 -1
- package/test/piece-completeness.test.js +37 -1
- package/test/quality-variants.test.js +1274 -1270
- package/test/rate-control-in-the-output.test.js +4 -3
- package/test/reads-not-yet-arrived.test.js +218 -218
- package/test/seek-frees-the-viewers-requests.test.js +1 -1
- package/test/segment-closed-on-both-branches.test.js +114 -114
- package/test/segment-judged-by-its-own-header.test.js +222 -222
- package/test/segment-serve-wiring.test.js +481 -481
- package/test/serving-existing-output.test.js +111 -111
- package/test/source-files-route.test.js +113 -113
- package/test/subtitle-cue-walk.test.js +385 -385
- package/test/subtitle-metadata.test.js +15 -15
- package/test/subtitle-plan-waits.test.js +125 -125
- package/test/subtitle-track-numbering.test.js +391 -391
- package/test/two-viewers-one-picture.test.js +508 -508
- package/test/viewer-requests-admission.test.js +92 -92
- package/test/viewer-starts-on-map.test.js +27 -27
- package/test/viewer-states.test.js +5 -2
- package/test/viewing-generations.test.js +390 -390
- package/test/whole-file-held-read.test.js +30 -30
- 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
|
+
}
|