@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
@@ -20,6 +20,7 @@ import { VideoTrack } from "../tracks/VideoTrack.js";
20
20
  import { AudioTrack } from "../tracks/AudioTrack.js";
21
21
  import { TextSubtitleTrack, TEXT_FORMATS_MP4 } from "../tracks/TextSubtitleTrack.js";
22
22
  import { ImageSubtitleTrack } from "../tracks/ImageSubtitleTrack.js";
23
+ import { isUnavailable } from "./unavailable.js";
23
24
 
24
25
  /** A box header is eight bytes, or sixteen when the size field says 1 (§4.2). */
25
26
  const HEADER_BYTES = 8;
@@ -199,8 +200,9 @@ export class Mp4Container extends Container {
199
200
  * @param {number} fileSize
200
201
  * @returns {Promise<number[]|null>} Null where the container has no index.
201
202
  */
202
- static readKeyframeTimes(readRange, fileSize) {
203
- return readMp4KeyframeTimes(readRange, fileSize);
203
+ static async readKeyframeTimes(readRange, fileSize) {
204
+ const index = await new Mp4Container({ readRange, fileSize }).parseKeyframeIndex();
205
+ return index ? index.times : null;
204
206
  }
205
207
 
206
208
  /**
@@ -211,8 +213,9 @@ export class Mp4Container extends Container {
211
213
  * @param {number} fileSize
212
214
  * @returns {Promise<{ tracks: object[] } | null>}
213
215
  */
214
- static readSubtitlePlan(readRange, fileSize) {
215
- return readMp4SubtitlePlan(readRange, fileSize);
216
+ static async readSubtitlePlan(readRange, fileSize) {
217
+ const held = await new Mp4Container({ readRange, fileSize }).#moovBuffer();
218
+ return held ? subtitlePlanFromMoov(held.moov) : null;
216
219
  }
217
220
 
218
221
  /**
@@ -224,7 +227,8 @@ export class Mp4Container extends Container {
224
227
  * @returns {Promise<object|null>}
225
228
  */
226
229
  async readSubtitlePlan() {
227
- const plan = await Mp4Container.readSubtitlePlan(this.readRange, this.fileSize);
230
+ const held = await this.#moovBuffer();
231
+ const plan = held ? subtitlePlanFromMoov(held.moov) : null;
228
232
  if (!plan) {
229
233
  return null;
230
234
  }
@@ -241,6 +245,7 @@ export class Mp4Container extends Container {
241
245
  declaredIndex: Number.isInteger(track.declaredIndex) ? track.declaredIndex : order,
242
246
  codecId: track.format,
243
247
  language: track.language,
248
+ languageSource: "language",
244
249
  name: "",
245
250
  isDefault: order === 0,
246
251
  codecPrivate: "",
@@ -262,46 +267,63 @@ export class Mp4Container extends Container {
262
267
  * reads per sample where Matroska reads per cluster.
263
268
  *
264
269
  * Nothing is fetched: a sample whose bytes are not downloaded is left for the
265
- * next call.
270
+ * next call, and so is one whose read did not complete — a sample is marked
271
+ * read only once its bytes are in hand.
266
272
  *
267
- * @param {{ codecId: string, samples: {offset: number, size: number, startSeconds: number, endSeconds: number}[] }} track
268
- * @param {Set<number>} harvested - Sample offsets already read; added to.
269
- * @returns {Promise<{startSeconds: number, endSeconds: number, text: string}[]>}
270
- * The cues found in THIS pass.
273
+ * @param {object} _plan
274
+ * @param {{ trackNumber: number, codecId: string, samples: {offset: number, size: number, startSeconds: number, endSeconds: number}[] }} track
275
+ * @param {object} progress
276
+ * @param {import("./Container.js").HeldReader} held
277
+ * @returns {Promise<{ found: Map<number, object[]>, covered: number, indexed: number, withdrawn: number[] }>}
271
278
  */
272
- async readHeldCues(_plan, track, progress) {
273
- let harvested = progress.harvested.get(track.trackNumber);
279
+ async readHeldCues(_plan, track, progress, held) {
280
+ if (!progress.mp4) {
281
+ progress.mp4 = { harvested: new Map() };
282
+ }
283
+ let harvested = progress.mp4.harvested.get(track.trackNumber);
274
284
  if (!harvested) {
275
285
  harvested = new Set();
276
- progress.harvested.set(track.trackNumber, harvested);
286
+ progress.mp4.harvested.set(track.trackNumber, harvested);
277
287
  }
278
- const cues = await this.readHeldSamples(track, harvested);
288
+ const cues = await this.readHeldSamples(track, harvested, held);
279
289
  return {
280
290
  found: cues.length > 0 ? new Map([[track.trackNumber, cues]]) : new Map(),
281
291
  covered: harvested.size,
282
- indexed: track?.samples?.length ?? 0
292
+ indexed: track?.samples?.length ?? 0,
293
+ withdrawn: []
283
294
  };
284
295
  }
285
296
 
286
- async readHeldSamples(track, harvested) {
297
+ /**
298
+ * @param {object} track
299
+ * @param {Set<number>} harvested
300
+ * @param {import("./Container.js").HeldReader} held
301
+ * @returns {Promise<{startSeconds: number, endSeconds: number, text: string, source: number}[]>}
302
+ */
303
+ async readHeldSamples(track, harvested, held) {
287
304
  const found = [];
288
305
  for (const sample of track?.samples ?? []) {
289
306
  if (harvested.has(sample.offset)) {
290
307
  continue;
291
308
  }
292
309
  const last = Math.min(this.fileSize - 1, sample.offset + sample.size - 1);
293
- if (!this.isHeld(sample.offset, last)) {
310
+ if (!held.isHeld(sample.offset, last)) {
294
311
  continue;
295
312
  }
296
- const bytes = await this.readHeld(sample.offset, last);
297
- if (!bytes) {
298
- continue;
313
+ let bytes;
314
+ try {
315
+ bytes = await held.read(sample.offset, last);
316
+ } catch (error) {
317
+ if (isUnavailable(error)) {
318
+ continue;
319
+ }
320
+ throw error;
299
321
  }
300
322
  harvested.add(sample.offset);
301
323
  // The MP4 has framed this cue and is the one that unframes it.
302
324
  const text = Mp4Container.cueTextOf(bytes, track.codecId);
303
325
  if (text) {
304
- found.push({ startSeconds: sample.startSeconds, endSeconds: sample.endSeconds, text });
326
+ found.push({ startSeconds: sample.startSeconds, endSeconds: sample.endSeconds, text, source: sample.offset });
305
327
  }
306
328
  }
307
329
  return found;
@@ -309,13 +331,14 @@ export class Mp4Container extends Container {
309
331
 
310
332
  async readTracks() {
311
333
  const head = await this.readRange(0, Math.min(64 - 1, this.fileSize - 1));
312
- if (!head || !isMp4(head)) return [];
334
+ if (!isMp4(head)) return [];
313
335
 
314
- // Use the subtitle plan reader's moov parsing as source for subtitle tracks,
315
- // and a direct moov walk for video/audio to collect tkhd/mdhd/hdlr/elng uniformly.
316
- // For simplicity, delegate entirely to readMp4SubtitlePlan for subtitles and
317
- // do a lightweight moov walk for video/audio here — then merge.
318
- const subtitlePlan = await readMp4SubtitlePlan(this.readRange, this.fileSize).catch(() => null);
336
+ // Subtitle tracks from the subtitle reading of the one `moov`, video and
337
+ // audio from a walk of the same buffer — the box is read once per file.
338
+ // A read that has not arrived is not swallowed here: it is not an answer
339
+ // about the file, and the caller keeps nothing for it.
340
+ const held = await this.#moovBuffer();
341
+ const subtitlePlan = held ? subtitlePlanFromMoov(held.moov) : null;
319
342
  const subtitleByDecl = new Map();
320
343
  if (subtitlePlan?.tracks) {
321
344
  for (const t of subtitlePlan.tracks) subtitleByDecl.set(t.declaredIndex, t);
@@ -336,6 +359,7 @@ export class Mp4Container extends Container {
336
359
  codecId: s.format,
337
360
  language: s.language,
338
361
  languageBcp47: "",
362
+ languageSource: "language",
339
363
  name: "",
340
364
  isEnabled: true,
341
365
  isDefault: s.declaredIndex === 0,
@@ -359,11 +383,18 @@ export class Mp4Container extends Container {
359
383
  }
360
384
 
361
385
  /**
362
- * The `moov` box, read whole.
386
+ * The `moov` box, read whole — the ONE reading of it every question here is
387
+ * answered from.
363
388
  *
364
389
  * Held on the instance because every question this class answers is inside
365
- * it, and the box can be tens of megabytes off a torrent — reading it once
366
- * per file is the difference between one fetch and one per question.
390
+ * it, and the box can be tens of megabytes off a torrent. Until 2026-10-01 it
391
+ * was read by three functions besides this one, each with its own rule for a
392
+ * read that failed.
393
+ *
394
+ * Three outcomes are kept: the box; null where the file has no `moov`; and
395
+ * null with `moovRefused` set where the box is larger than this program reads
396
+ * whole — a limit of ours, said as one, not an absence. A read whose bytes
397
+ * have not arrived throws `BytesUnavailable` and keeps nothing.
367
398
  *
368
399
  * @returns {Promise<{ moov: Buffer, header: number } | null>}
369
400
  */
@@ -371,31 +402,18 @@ export class Mp4Container extends Container {
371
402
  if (this.moovHeld !== undefined) {
372
403
  return this.moovHeld;
373
404
  }
374
- const PROBE = 64;
375
- const MAX_MOOV = 32 * 1024 * 1024;
376
- let at = 0;
377
- let moovBox = null;
378
- while (at < this.fileSize) {
379
- const probe = await this.readRange(at, Math.min(this.fileSize - 1, at + PROBE - 1));
380
- if (!probe || probe.length < 8) break;
381
- let size = probe.readUInt32BE(0);
382
- const type = probe.toString("latin1", 4, 8);
383
- let header = 8;
384
- if (size === 1) {
385
- if (probe.length < 16) break;
386
- size = Number(probe.readBigUInt64BE(8));
387
- header = 16;
388
- }
389
- if (size <= 0) break;
390
- if (type === "moov") { moovBox = { offset: at, size, header }; break; }
391
- at += size;
405
+ const found = await findMoov(this.readRange, this.fileSize);
406
+ if (!found) {
407
+ this.moovHeld = null;
408
+ return null;
392
409
  }
393
- if (!moovBox || moovBox.size > MAX_MOOV) {
410
+ if (found.size > MAX_MOOV_BYTES) {
411
+ this.moovRefused = { size: found.size, limit: MAX_MOOV_BYTES };
394
412
  this.moovHeld = null;
395
413
  return null;
396
414
  }
397
- const moov = await this.readRange(moovBox.offset, Math.min(this.fileSize - 1, moovBox.offset + moovBox.size - 1));
398
- this.moovHeld = moov ? { moov, header: moovBox.header } : null;
415
+ const moov = await this.readRange(found.offset, found.offset + found.size - 1);
416
+ this.moovHeld = { moov, header: found.headerBytes };
399
417
  return this.moovHeld;
400
418
  }
401
419
 
@@ -417,8 +435,10 @@ export class Mp4Container extends Container {
417
435
  }
418
436
  /** @type {import("./Container.js").ContainerMediaInfo} */
419
437
  const info = { format: this.formatName, durationSeconds: null, startTimeSeconds: null };
420
- this.mediaInfo = info;
421
438
  const held = await this.#moovBuffer();
439
+ // Kept only once the `moov` reading has an outcome: a read that had not
440
+ // arrived used to leave an empty answer here for the life of the container.
441
+ this.mediaInfo = info;
422
442
  if (!held) {
423
443
  return info;
424
444
  }
@@ -620,7 +640,8 @@ export class Mp4Container extends Container {
620
640
  }
621
641
 
622
642
  async parseKeyframeIndex() {
623
- const r = await readMp4KeyframeTimes(this.readRange, this.fileSize);
643
+ const held = await this.#moovBuffer();
644
+ const r = held ? keyframeTimesFromMoov(held.moov, held.header) : null;
624
645
  if (!r) return null;
625
646
  if (Array.isArray(r)) return { times: r, tolerance: 0 };
626
647
  return r;
@@ -788,16 +809,10 @@ function sampleOffsets(moov, stsc, chunkOffsets, sizes) {
788
809
  /**
789
810
  * The text subtitle tracks of an MP4, with every cue's time and byte range.
790
811
  *
791
- * @param {(start: number, end: number) => Promise<Buffer | null>} readRange
792
- * @param {number} fileSize
793
- * @returns {Promise<{ tracks: Mp4SubtitleTrack[] } | null>}
812
+ * @param {Buffer} moov - The whole `moov` box, its header included.
813
+ * @returns {{ tracks: Mp4SubtitleTrack[] } | null}
794
814
  */
795
- async function readMp4SubtitlePlan(readRange, fileSize) {
796
- const found = await findMoov(readRange, fileSize);
797
- if (!found || found.size > MAX_MOOV_BYTES) {
798
- return null;
799
- }
800
- const moov = await readRange(found.offset, Math.min(fileSize - 1, found.offset + found.size - 1));
815
+ function subtitlePlanFromMoov(moov) {
801
816
  if (!moov || moov.length < HEADER_BYTES) {
802
817
  return null;
803
818
  }
@@ -1254,20 +1269,15 @@ function isVideoTrack(buffer, mdia) {
1254
1269
  }
1255
1270
 
1256
1271
  /**
1257
- * Read the keyframe times of an MP4/MOV file.
1272
+ * Read the keyframe times of an MP4/MOV file from its `moov` box.
1258
1273
  *
1259
- * @param {(start: number, end: number) => Promise<Buffer | null>} readRange
1260
- * @param {number} fileSize
1261
- * @returns {Promise<number[] | null>} Ascending seconds, or null when the file
1274
+ * @param {Buffer} moov - The whole `moov` box, its header included.
1275
+ * @param {number} headerBytes - The box header's length.
1276
+ * @returns {number[] | null} Ascending seconds, or null when the file
1262
1277
  * carries no usable index (fragmented MP4, truncated or damaged `moov`).
1263
1278
  */
1264
- async function readMp4KeyframeTimes(readRange, fileSize) {
1265
- const moovBox = await findMoov(readRange, fileSize);
1266
- if (!moovBox || moovBox.size > MAX_MOOV_BYTES) {
1267
- return null;
1268
- }
1269
-
1270
- const moov = await readRange(moovBox.offset, Math.min(fileSize - 1, moovBox.offset + moovBox.size - 1));
1279
+ function keyframeTimesFromMoov(moov, headerBytes) {
1280
+ const moovBox = { headerBytes };
1271
1281
  if (!moov || moov.length < moovBox.headerBytes) {
1272
1282
  return null;
1273
1283
  }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @file EBML elements read where they lie, a portion at a time.
3
+ *
4
+ * `ebml-reader.js` parses a buffer the caller already holds. That is enough for
5
+ * a head window, and it is not enough for the elements that matter here: a Cues
6
+ * table whose size is the file's business, a Tracks element pushed out of the
7
+ * head by a large CodecPrivate, a cluster of a high-bitrate film. Reading any
8
+ * of them "in one go" needs a bound, and a bound is a statement about this
9
+ * program dressed up as a statement about the file — an element over it was
10
+ * reported as absent.
11
+ *
12
+ * This reads element HEADERS one at a time and element DATA only when asked,
13
+ * through a window no larger than one portion. The portion is handed in by
14
+ * whoever knows the medium (a torrent's piece length) and is never chosen here.
15
+ * Skipping an element costs one header read; an element whose data is wanted
16
+ * and is larger than a portion is refused by name, with its size.
17
+ *
18
+ * Grammar only, like `ebml-reader.js`: nothing here knows Matroska's ids.
19
+ */
20
+
21
+ import { readVint } from "./ebml-reader.js";
22
+
23
+ /** An element id is at most four bytes and a size at most eight (RFC 8794 §4). */
24
+ export const MAX_HEADER_BYTES = 12;
25
+
26
+ /**
27
+ * Whether a size field says "unknown" — every value bit set (RFC 8794 §6.2).
28
+ *
29
+ * @param {{ value: number, length: number }} size
30
+ * @returns {boolean}
31
+ */
32
+ export function sizeIsUnknown(size) {
33
+ return size.value === 2 ** (7 * size.length) - 1;
34
+ }
35
+
36
+ /**
37
+ * @typedef {object} ElementHeader
38
+ * @property {number} id
39
+ * @property {number} at - Where the element begins, its id included.
40
+ * @property {number} dataOffset - Where its data begins.
41
+ * @property {number | null} size - Null when the size is unknown.
42
+ * @property {number | null} end - One past the last data byte, null when unknown.
43
+ */
44
+
45
+ export class ElementReader {
46
+ /** @type {(start: number, end: number) => Promise<Buffer>} */
47
+ #read;
48
+
49
+ /** @type {number} */
50
+ #fileSize;
51
+
52
+ /** @type {number} */
53
+ #portion;
54
+
55
+ /** @type {boolean} */
56
+ #prefetch;
57
+
58
+ /** @type {(start: number) => number} */
59
+ #readableUntil;
60
+
61
+ /** @type {Buffer | null} */
62
+ #window = null;
63
+
64
+ /** @type {number} */
65
+ #windowAt = 0;
66
+
67
+ /**
68
+ * @param {object} params
69
+ * @param {(start: number, end: number) => Promise<Buffer>} params.read - Answers
70
+ * with every byte of the range or throws (`strictReader`).
71
+ * @param {number} params.fileSize
72
+ * @param {number} [params.portionBytes] - The largest read this makes. Absent,
73
+ * an element is read in one go, which is right for a file on a local disk.
74
+ * @param {boolean} [params.prefetch] - Read a whole portion on a miss. Right
75
+ * when every byte of a span will be looked at (a cluster being walked);
76
+ * wrong when only a few headers will (the elements of a Segment, where a
77
+ * fetching read of a portion per header would pull megabytes for nothing).
78
+ * @param {(start: number) => number} [params.readableUntil] - One past the
79
+ * last byte that can be read from `start` in one go. A read of what is
80
+ * downloaded must not run a prefetch past the end of the downloaded run,
81
+ * or a cluster that is whole would fail for the bytes after it.
82
+ */
83
+ constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil }) {
84
+ this.#read = read;
85
+ this.#fileSize = fileSize;
86
+ this.#readableUntil = typeof readableUntil === "function" ? readableUntil : () => fileSize;
87
+ this.#portion = Number.isFinite(portionBytes) && portionBytes > 0 ? portionBytes : Number.POSITIVE_INFINITY;
88
+ this.#prefetch = prefetch;
89
+ }
90
+
91
+ /** @returns {number} */
92
+ get portionBytes() {
93
+ return this.#portion;
94
+ }
95
+
96
+ /**
97
+ * `length` bytes from `start`, or fewer where the file ends first.
98
+ *
99
+ * @param {number} start
100
+ * @param {number} length
101
+ * @returns {Promise<Buffer>}
102
+ */
103
+ async bytes(start, length) {
104
+ const end = Math.min(this.#fileSize, start + length);
105
+ if (end <= start) {
106
+ return Buffer.alloc(0);
107
+ }
108
+ const window = this.#window;
109
+ if (window && start >= this.#windowAt && end <= this.#windowAt + window.length) {
110
+ return window.subarray(start - this.#windowAt, end - this.#windowAt);
111
+ }
112
+ const reach = Math.min(this.#fileSize, Math.max(end, this.#readableUntil(start)));
113
+ const span = this.#prefetch ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
114
+ const got = await this.#read(start, start + span - 1);
115
+ this.#window = got;
116
+ this.#windowAt = start;
117
+ return got.subarray(0, end - start);
118
+ }
119
+
120
+ /**
121
+ * The element whose header begins at `at`.
122
+ *
123
+ * @param {number} at
124
+ * @param {number} limit - One past the last byte the element may occupy.
125
+ * @returns {Promise<ElementHeader | null>} Null when the bytes there are not
126
+ * an element header — a malformed vint, or no room for one.
127
+ */
128
+ async header(at, limit) {
129
+ const room = Math.min(limit, this.#fileSize) - at;
130
+ if (room <= 1) {
131
+ return null;
132
+ }
133
+ const bytes = await this.bytes(at, Math.min(MAX_HEADER_BYTES, room));
134
+ const id = readVint(bytes, 0, true);
135
+ if (!id) {
136
+ return null;
137
+ }
138
+ const size = readVint(bytes, id.length, false);
139
+ if (!size) {
140
+ return null;
141
+ }
142
+ const dataOffset = at + id.length + size.length;
143
+ if (sizeIsUnknown(size)) {
144
+ return { id: id.value, at, dataOffset, size: null, end: null };
145
+ }
146
+ return { id: id.value, at, dataOffset, size: size.value, end: dataOffset + size.value };
147
+ }
148
+
149
+ /**
150
+ * An element's data, copied out of the window so the window can move on.
151
+ *
152
+ * @param {ElementHeader} element
153
+ * @returns {Promise<Buffer | null>} Null when the data is larger than one
154
+ * portion: refused, never assembled.
155
+ */
156
+ async data(element) {
157
+ if (element.size === null || element.size > this.#portion) {
158
+ return null;
159
+ }
160
+ return Buffer.from(await this.bytes(element.dataOffset, element.size));
161
+ }
162
+
163
+ /**
164
+ * The first `length` bytes of an element's data, without reading the rest.
165
+ *
166
+ * @param {ElementHeader} element
167
+ * @param {number} length
168
+ * @returns {Promise<Buffer>}
169
+ */
170
+ async peek(element, length) {
171
+ const available = element.size === null ? length : Math.min(length, element.size);
172
+ return this.bytes(element.dataOffset, available);
173
+ }
174
+ }