mkvpeek 0.1.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/browser.d.ts +12 -0
  4. package/dist/browser.js +3 -0
  5. package/dist/ebml.d.ts +168 -0
  6. package/dist/ebml.js +224 -0
  7. package/dist/entry/browser.d.ts +44 -0
  8. package/dist/entry/browser.js +9 -0
  9. package/dist/entry/node.d.ts +37 -0
  10. package/dist/entry/node.js +38 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.js +3 -0
  13. package/dist/io/file.d.ts +2 -0
  14. package/dist/io/file.js +47 -0
  15. package/dist/io/lanes.d.ts +17 -0
  16. package/dist/io/lanes.js +124 -0
  17. package/dist/io/memory.d.ts +2 -0
  18. package/dist/io/memory.js +16 -0
  19. package/dist/io/range.d.ts +22 -0
  20. package/dist/io/range.js +190 -0
  21. package/dist/io/source.d.ts +102 -0
  22. package/dist/io/source.js +31 -0
  23. package/dist/io/url-node.d.ts +10 -0
  24. package/dist/io/url-node.js +99 -0
  25. package/dist/io/url.d.ts +21 -0
  26. package/dist/io/url.js +80 -0
  27. package/dist/io/windows.d.ts +37 -0
  28. package/dist/io/windows.js +105 -0
  29. package/dist/matroska/block.d.ts +47 -0
  30. package/dist/matroska/block.js +114 -0
  31. package/dist/matroska/chain.d.ts +55 -0
  32. package/dist/matroska/chain.js +47 -0
  33. package/dist/matroska/clusters.d.ts +4 -0
  34. package/dist/matroska/clusters.js +232 -0
  35. package/dist/matroska/cues.d.ts +23 -0
  36. package/dist/matroska/cues.js +93 -0
  37. package/dist/matroska/header.d.ts +52 -0
  38. package/dist/matroska/header.js +531 -0
  39. package/dist/peek/contract.d.ts +57 -0
  40. package/dist/peek/contract.js +24 -0
  41. package/dist/peek/engine.d.ts +19 -0
  42. package/dist/peek/engine.js +32 -0
  43. package/dist/peek/target.d.ts +11 -0
  44. package/dist/peek/target.js +56 -0
  45. package/dist/subtitle/assemble.d.ts +21 -0
  46. package/dist/subtitle/assemble.js +114 -0
  47. package/dist/subtitle/conclude.d.ts +20 -0
  48. package/dist/subtitle/conclude.js +57 -0
  49. package/dist/subtitle/indexed.d.ts +5 -0
  50. package/dist/subtitle/indexed.js +172 -0
  51. package/dist/subtitle/options.d.ts +76 -0
  52. package/dist/subtitle/options.js +1 -0
  53. package/dist/subtitle/peek.d.ts +5 -0
  54. package/dist/subtitle/peek.js +107 -0
  55. package/dist/subtitle/tuning.d.ts +4 -0
  56. package/dist/subtitle/tuning.js +76 -0
  57. package/dist/subtitle/walked.d.ts +5 -0
  58. package/dist/subtitle/walked.js +28 -0
  59. package/dist/tracks/core.d.ts +25 -0
  60. package/dist/tracks/core.js +24 -0
  61. package/dist/tracks/list.d.ts +8 -0
  62. package/dist/tracks/list.js +6 -0
  63. package/dist/tracks/subtitle.d.ts +45 -0
  64. package/dist/tracks/subtitle.js +38 -0
  65. package/dist/vocabulary.d.ts +88 -0
  66. package/dist/vocabulary.js +10 -0
  67. package/package.json +68 -0
@@ -0,0 +1,531 @@
1
+ import * as E from "../ebml.js";
2
+ import { gathered } from "../io/lanes.js";
3
+ import { HEAD_BYTES } from "../io/source.js";
4
+ import { windowAt } from "../io/windows.js";
5
+ import { CLUSTER_PROBE_BYTES, headerNeed, hopTopLevel, pastSegment, readClusterTime, readPastHead, } from "./chain.js";
6
+ export async function readContainer(source, depth, { concurrency = 1, signal: stop, ...wants }) {
7
+ const opened = await openContainer(source, stop);
8
+ if (typeof opened === "string")
9
+ return opened;
10
+ const { segment } = opened;
11
+ const noTags = new Map();
12
+ const noCues = new Map();
13
+ const noInfo = emptyInfo();
14
+ const noAttachments = [];
15
+ const parseCuesIn = (w, el) => parseCues(w, el, segment);
16
+ const wantsInfo = wants.info || depth !== "tracks";
17
+ const fetchTrackTags = () => fetchedOptional(opened, E.ID.segment.TAGS, noTags, parseTrackTags);
18
+ const fetchInfo = wantsInfo
19
+ ? () => fetchedOptional(opened, E.ID.segment.INFO, noInfo, parseInfo)
20
+ : () => Promise.resolve(noInfo);
21
+ const fetchAttachments = wants.attachments
22
+ ? () => readAttachments(opened)
23
+ : () => Promise.resolve(noAttachments);
24
+ const fetchTimedFields = depth !== "tracks" ? () => readTimedFields(opened) : () => Promise.resolve(null);
25
+ const fetchCues = depth === "indexed"
26
+ ? () => fetchedOptional(opened, E.ID.segment.CUES, noCues, parseCuesIn)
27
+ : () => Promise.resolve(null);
28
+ const fetches = [
29
+ fetchTrackTags,
30
+ fetchInfo,
31
+ fetchAttachments,
32
+ fetchTimedFields,
33
+ fetchCues,
34
+ ];
35
+ const [trackTags, info, attachments, timed, cues] = await gathered(fetches, concurrency, stop);
36
+ return {
37
+ ...opened.headerSoFar,
38
+ trackTags,
39
+ info,
40
+ attachments,
41
+ ...(timed === null ? {} : { head: opened.head, ...timed }),
42
+ ...(cues === null ? {} : { cues }),
43
+ };
44
+ }
45
+ const ELEMENT_PROBE_BYTES = 4096;
46
+ const MAX_HEADER_ELEMENT_BYTES = 64 * 1024 ** 2;
47
+ const MAX_ATTACHMENT_CHILDREN = 4096;
48
+ const emptyInfo = () => ({
49
+ title: null,
50
+ muxingApp: null,
51
+ writingApp: null,
52
+ durationMs: null,
53
+ timestampScale: E.DEFAULT_TIMESTAMP_SCALE,
54
+ });
55
+ const readsAs = (el, id) => E.decoded(el) && el.id === id;
56
+ const nulTrimmed = (padded) => padded.replace(/\0+$/, "");
57
+ const fetchedOptional = (opened, id, fallback, parse) => optional(fallback, async (fall) => {
58
+ const el = await fetchElement(opened, id);
59
+ if (el === null)
60
+ return fall;
61
+ return parse(el.window, el.element) ?? fall;
62
+ });
63
+ function probed(opened, at, limit, held) {
64
+ const need = headerNeed(at, limit);
65
+ if (E.covers(held, at, need))
66
+ return Promise.resolve(held);
67
+ const width = Math.min(ELEMENT_PROBE_BYTES, limit - at);
68
+ return readPastHead(opened, at, width, held);
69
+ }
70
+ function flagOf(window, field) {
71
+ const value = E.readUint(window, field);
72
+ return value === null ? null : value === 1;
73
+ }
74
+ function simpleTag(window, { size, body }) {
75
+ let name = "";
76
+ let value = "";
77
+ for (const part of E.children(window, body, size)) {
78
+ if (part.id === E.ID.tag.NAME) {
79
+ name = E.readUtf8(window, part);
80
+ }
81
+ else if (part.id === E.ID.tag.STRING) {
82
+ value = E.readUtf8(window, part);
83
+ }
84
+ }
85
+ return { name: nulTrimmed(name), value: nulTrimmed(value) };
86
+ }
87
+ async function openContainer(source, signal) {
88
+ const fileSize = await source.size();
89
+ const head = await windowAt({ source, fileSize }, 0, HEAD_BYTES);
90
+ const ebml = E.readElement(head, 0);
91
+ if (!readsAs(ebml, E.ID.root.EBML))
92
+ return "not-matroska";
93
+ const segmentEl = E.readElement(head, ebml.next);
94
+ if (typeof segmentEl === "string" || segmentEl.id !== E.ID.root.SEGMENT) {
95
+ return "malformed";
96
+ }
97
+ const segment = {
98
+ body: segmentEl.body,
99
+ end: "open" in segmentEl ? fileSize : segmentEl.next,
100
+ };
101
+ const located = locateTopLevel(head, segment);
102
+ const pass = { source, segment, head, signal, offsets: located };
103
+ const tracksEl = await fetchElement(pass, E.ID.segment.TRACKS);
104
+ if (tracksEl === null)
105
+ return "malformed";
106
+ const tracks = parseTracks(tracksEl.window, tracksEl.element);
107
+ if (typeof tracks === "string")
108
+ return tracks;
109
+ return { ...pass, headerSoFar: { segment, trackEntries: tracks, fileSize } };
110
+ }
111
+ async function readTimedFields(opened) {
112
+ const firstClusterTicks = await optional(null, () => readFirstClusterTime(opened));
113
+ return { firstClusterTicks };
114
+ }
115
+ async function optional(fallback, read) {
116
+ try {
117
+ return await read(fallback);
118
+ }
119
+ catch (error) {
120
+ if (!E.isGrammarError(error))
121
+ throw error;
122
+ return fallback;
123
+ }
124
+ }
125
+ async function readFirstClusterTime(opened) {
126
+ let at = opened.offsets.get(E.ID.segment.CLUSTER);
127
+ let held = opened.head;
128
+ if (at === undefined) {
129
+ for await (const hop of hopTopLevel(opened)) {
130
+ if (hop.el.id === E.ID.segment.CLUSTER) {
131
+ at = hop.at;
132
+ held = hop.window;
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ if (at === undefined)
138
+ return null;
139
+ const window = await readPastHead(opened, at, CLUSTER_PROBE_BYTES, held);
140
+ const cluster = E.readElement(window, at);
141
+ if (typeof cluster === "string" || cluster.id !== E.ID.segment.CLUSTER)
142
+ return null;
143
+ return readClusterTime(window, cluster);
144
+ }
145
+ function locateTopLevel(head, segment) {
146
+ const offsets = new Map();
147
+ let pos = segment.body;
148
+ const seekHeads = [];
149
+ while (pos < segment.end) {
150
+ const el = E.readElement(head, pos);
151
+ if (typeof el === "string")
152
+ break;
153
+ if ("open" in el) {
154
+ if (el.id === E.ID.segment.CLUSTER && !offsets.has(el.id)) {
155
+ offsets.set(el.id, pos);
156
+ }
157
+ break;
158
+ }
159
+ if (pastSegment(el, segment))
160
+ break;
161
+ if (!offsets.has(el.id)) {
162
+ offsets.set(el.id, pos);
163
+ }
164
+ if (el.id === E.ID.segment.SEEK_HEAD)
165
+ seekHeads.push(el);
166
+ if (el.id === E.ID.segment.CLUSTER)
167
+ break;
168
+ pos = el.next;
169
+ }
170
+ try {
171
+ for (const seekHead of seekHeads) {
172
+ for (const seek of E.children(head, seekHead.body, seekHead.size)) {
173
+ if (seek.id !== E.ID.seek.ENTRY)
174
+ continue;
175
+ let id = null;
176
+ let position = null;
177
+ for (const field of E.children(head, seek.body, seek.size)) {
178
+ if (field.id === E.ID.seek.ID) {
179
+ id = E.readUint(head, field);
180
+ }
181
+ else if (field.id === E.ID.seek.POSITION) {
182
+ position = E.readUint(head, field);
183
+ }
184
+ }
185
+ if (id !== null && position !== null && !offsets.has(id)) {
186
+ offsets.set(id, segment.body + position);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ catch (error) {
192
+ if (!E.isGrammarError(error))
193
+ throw error;
194
+ }
195
+ return offsets;
196
+ }
197
+ function parseCues(window, cues, segment) {
198
+ const byTrack = new Map();
199
+ const { bytes } = window;
200
+ const startOf = (at, need) => {
201
+ const start = at - window.at;
202
+ if (start < 0 || start + need > bytes.length)
203
+ throw new E.GrammarError();
204
+ return start;
205
+ };
206
+ const cursor = { id: 0, body: 0, next: 0 };
207
+ const readElementHead = (at, end) => {
208
+ const idStart = startOf(at, 1);
209
+ const idLength = E.vintLength(bytes[idStart]);
210
+ if (idLength > E.MAX_VINT_BYTES)
211
+ throw new E.GrammarError();
212
+ startOf(at, idLength);
213
+ cursor.id = E.vintValue(bytes, idStart, idLength, "id");
214
+ const sizeAt = at + idLength;
215
+ const sizeStart = startOf(sizeAt, 1);
216
+ const sizeLength = E.vintLength(bytes[sizeStart]);
217
+ if (sizeLength > E.MAX_VINT_BYTES)
218
+ throw new E.GrammarError();
219
+ startOf(sizeAt, sizeLength);
220
+ if (E.vintAllOnes(bytes, sizeStart, sizeLength))
221
+ throw new E.GrammarError();
222
+ cursor.body = sizeAt + sizeLength;
223
+ cursor.next = cursor.body + E.vintValue(bytes, sizeStart, sizeLength, "size");
224
+ if (cursor.next > end)
225
+ throw new E.GrammarError();
226
+ };
227
+ const end = cues.body + cues.size;
228
+ let at = cues.body;
229
+ while (at < end) {
230
+ readElementHead(at, end);
231
+ const pointId = cursor.id;
232
+ const pointBody = cursor.body;
233
+ const pointNext = cursor.next;
234
+ if (pointId === E.ID.cue.POINT) {
235
+ let fieldAt = pointBody;
236
+ while (fieldAt < pointNext) {
237
+ readElementHead(fieldAt, pointNext);
238
+ const fieldId = cursor.id;
239
+ const fieldBody = cursor.body;
240
+ const fieldNext = cursor.next;
241
+ if (fieldId === E.ID.cue.TRACK_POSITIONS) {
242
+ let track = null;
243
+ let clusterPos = null;
244
+ let relPos = 0;
245
+ let itemAt = fieldBody;
246
+ while (itemAt < fieldNext) {
247
+ readElementHead(itemAt, fieldNext);
248
+ const itemId = cursor.id;
249
+ const size = cursor.next - cursor.body;
250
+ const start = startOf(cursor.body, size);
251
+ const value = E.uintFits(size) ? E.uintAt(bytes, start, size) : null;
252
+ if (itemId === E.ID.cue.TRACK) {
253
+ track = value;
254
+ }
255
+ else if (itemId === E.ID.cue.CLUSTER_POSITION) {
256
+ clusterPos = value;
257
+ }
258
+ else if (itemId === E.ID.cue.RELATIVE_POSITION) {
259
+ relPos = value;
260
+ }
261
+ itemAt = cursor.next;
262
+ }
263
+ if (track === null || clusterPos === null || relPos === null)
264
+ return null;
265
+ const entry = { clusterPos: segment.body + clusterPos, relPos };
266
+ const list = byTrack.get(track);
267
+ if (list === undefined)
268
+ byTrack.set(track, [entry]);
269
+ else
270
+ list.push(entry);
271
+ }
272
+ fieldAt = fieldNext;
273
+ }
274
+ }
275
+ at = pointNext;
276
+ }
277
+ return byTrack;
278
+ }
279
+ async function fetchElement(opened, id) {
280
+ const at = opened.offsets.get(id);
281
+ if (at === undefined)
282
+ return null;
283
+ const probe = await probed(opened, at, opened.head.fileSize, opened.head);
284
+ const header = E.readElement(probe, at);
285
+ if (!readsAs(header, id))
286
+ return null;
287
+ if (header.next - at > MAX_HEADER_ELEMENT_BYTES)
288
+ return null;
289
+ const window = await readPastHead(opened, at, header.next - at, probe);
290
+ const element = E.readElement(window, at);
291
+ return E.decoded(element) ? { window, element } : null;
292
+ }
293
+ function parseTracks(window, tracks) {
294
+ const entries = [];
295
+ const numbers = new Set();
296
+ let streamIndex = 0;
297
+ for (const entry of E.children(window, tracks.body, tracks.size)) {
298
+ if (entry.id !== E.ID.track.ENTRY)
299
+ continue;
300
+ const track = {
301
+ streamIndex: streamIndex++,
302
+ number: -1,
303
+ uid: null,
304
+ codecId: "",
305
+ codecPrivate: "",
306
+ codecDelayNs: 0,
307
+ type: -1,
308
+ language: null,
309
+ languageIetf: null,
310
+ name: null,
311
+ flags: {
312
+ enabled: true,
313
+ default: true,
314
+ forced: false,
315
+ hearingImpaired: null,
316
+ visualImpaired: null,
317
+ textDescriptions: null,
318
+ original: null,
319
+ commentary: null,
320
+ },
321
+ encoded: false,
322
+ };
323
+ for (const field of E.children(window, entry.body, entry.size)) {
324
+ const { id } = field;
325
+ if (id === E.ID.track.NUMBER) {
326
+ const number = E.readUint(window, field);
327
+ if (number === null)
328
+ return "malformed";
329
+ track.number = number;
330
+ }
331
+ else if (id === E.ID.track.UID) {
332
+ track.uid = E.readUid(window, field);
333
+ }
334
+ else if (id === E.ID.track.TYPE) {
335
+ const type = E.readUint(window, field);
336
+ if (type === null)
337
+ return "malformed";
338
+ track.type = type;
339
+ }
340
+ else if (id === E.ID.track.CODEC_ID) {
341
+ track.codecId = E.readAscii(window, field);
342
+ }
343
+ else if (id === E.ID.track.CODEC_PRIVATE) {
344
+ track.codecPrivate = E.readUtf8(window, field);
345
+ }
346
+ else if (id === E.ID.track.CODEC_DELAY) {
347
+ const delay = E.readUint(window, field);
348
+ if (delay === null)
349
+ return "malformed";
350
+ track.codecDelayNs = delay;
351
+ }
352
+ else if (id === E.ID.track.LANGUAGE) {
353
+ track.language = E.readAscii(window, field);
354
+ }
355
+ else if (id === E.ID.track.LANGUAGE_IETF) {
356
+ track.languageIetf = E.readAscii(window, field);
357
+ }
358
+ else if (id === E.ID.track.NAME) {
359
+ track.name = E.readUtf8(window, field);
360
+ }
361
+ else if (id === E.ID.track.FLAG_FORCED) {
362
+ track.flags.forced = flagOf(window, field) ?? track.flags.forced;
363
+ }
364
+ else if (id === E.ID.track.FLAG_DEFAULT) {
365
+ track.flags.default = flagOf(window, field) ?? track.flags.default;
366
+ }
367
+ else if (id === E.ID.track.FLAG_ENABLED) {
368
+ track.flags.enabled = flagOf(window, field) ?? track.flags.enabled;
369
+ }
370
+ else if (id === E.ID.track.FLAG_HEARING_IMPAIRED) {
371
+ track.flags.hearingImpaired = flagOf(window, field) ?? track.flags.hearingImpaired;
372
+ }
373
+ else if (id === E.ID.track.FLAG_VISUAL_IMPAIRED) {
374
+ track.flags.visualImpaired = flagOf(window, field) ?? track.flags.visualImpaired;
375
+ }
376
+ else if (id === E.ID.track.FLAG_TEXT_DESCRIPTIONS) {
377
+ track.flags.textDescriptions = flagOf(window, field) ?? track.flags.textDescriptions;
378
+ }
379
+ else if (id === E.ID.track.FLAG_ORIGINAL) {
380
+ track.flags.original = flagOf(window, field) ?? track.flags.original;
381
+ }
382
+ else if (id === E.ID.track.FLAG_COMMENTARY) {
383
+ track.flags.commentary = flagOf(window, field) ?? track.flags.commentary;
384
+ }
385
+ else if (id === E.ID.track.CONTENT_ENCODINGS) {
386
+ track.encoded = true;
387
+ }
388
+ }
389
+ if (track.number < 0)
390
+ return "malformed";
391
+ if (numbers.has(track.number))
392
+ return "malformed";
393
+ numbers.add(track.number);
394
+ entries.push(track);
395
+ }
396
+ return entries;
397
+ }
398
+ function parseTimestampScale(window, info) {
399
+ for (const field of E.children(window, info.body, info.size)) {
400
+ if (field.id !== E.ID.info.TIMESTAMP_SCALE)
401
+ continue;
402
+ const scale = E.readUint(window, field);
403
+ if (scale !== null && scale > 0)
404
+ return scale;
405
+ }
406
+ return E.DEFAULT_TIMESTAMP_SCALE;
407
+ }
408
+ async function eachChildIn(opened, parent, held, visit) {
409
+ let window = held;
410
+ let pos = parent.body;
411
+ let children = 0;
412
+ while (pos < parent.next) {
413
+ children += 1;
414
+ if (children > MAX_ATTACHMENT_CHILDREN)
415
+ return null;
416
+ window = await probed(opened, pos, parent.next, window);
417
+ const field = E.readElement(window, pos);
418
+ if (!E.decoded(field))
419
+ return window;
420
+ if (!E.fits(field, parent.next))
421
+ return null;
422
+ const next = await visit(pos, field, window);
423
+ if (next === null)
424
+ return null;
425
+ window = next;
426
+ pos = field.next;
427
+ }
428
+ return window;
429
+ }
430
+ async function readAttachments(opened) {
431
+ const at = opened.offsets.get(E.ID.segment.ATTACHMENTS);
432
+ if (at === undefined)
433
+ return [];
434
+ const read = async (fallback) => {
435
+ const window = await probed(opened, at, opened.head.fileSize, opened.head);
436
+ const attachments = E.readElement(window, at);
437
+ if (!readsAs(attachments, E.ID.segment.ATTACHMENTS))
438
+ return fallback;
439
+ const out = [];
440
+ const visit = async (_at, fileEl, held) => {
441
+ if (fileEl.id !== E.ID.attachment.FILE)
442
+ return held;
443
+ const file = await readAttachedFile(opened, fileEl, held);
444
+ if (file === null)
445
+ return null;
446
+ out.push(file.file);
447
+ return file.window;
448
+ };
449
+ const last = await eachChildIn(opened, attachments, window, visit);
450
+ return last === null ? fallback : out;
451
+ };
452
+ return optional([], read);
453
+ }
454
+ async function readAttachedFile(opened, fileEl, held) {
455
+ const out = { fileName: null, mimeType: null, description: null, size: 0, uid: null };
456
+ const visit = async (at, field, carried) => {
457
+ if (field.id === E.ID.attachment.FILE_DATA) {
458
+ out.size = field.size;
459
+ return carried;
460
+ }
461
+ if (field.next - at > ELEMENT_PROBE_BYTES)
462
+ return carried;
463
+ const window = await readPastHead(opened, at, field.next - at, carried);
464
+ if (field.id === E.ID.attachment.FILE_NAME) {
465
+ out.fileName = nulTrimmed(E.readUtf8(window, field));
466
+ }
467
+ else if (field.id === E.ID.attachment.FILE_MIME) {
468
+ out.mimeType = E.readAscii(window, field);
469
+ }
470
+ else if (field.id === E.ID.attachment.FILE_DESCRIPTION) {
471
+ out.description = nulTrimmed(E.readUtf8(window, field));
472
+ }
473
+ else if (field.id === E.ID.attachment.FILE_UID) {
474
+ out.uid = E.readUid(window, field);
475
+ }
476
+ return window;
477
+ };
478
+ const last = await eachChildIn(opened, fileEl, held, visit);
479
+ return last === null ? null : { file: out, window: last };
480
+ }
481
+ function parseInfo(window, info) {
482
+ const scale = parseTimestampScale(window, info);
483
+ const out = { ...emptyInfo(), timestampScale: scale };
484
+ for (const field of E.children(window, info.body, info.size)) {
485
+ const { id } = field;
486
+ if (id === E.ID.info.TITLE) {
487
+ out.title = nulTrimmed(E.readUtf8(window, field));
488
+ }
489
+ else if (id === E.ID.info.MUXING_APP) {
490
+ out.muxingApp = nulTrimmed(E.readUtf8(window, field));
491
+ }
492
+ else if (id === E.ID.info.WRITING_APP) {
493
+ out.writingApp = nulTrimmed(E.readUtf8(window, field));
494
+ }
495
+ else if (id === E.ID.info.DURATION) {
496
+ const ticks = E.readFloat(window, field);
497
+ if (ticks !== null) {
498
+ const ms = (ticks * scale) / 1_000_000;
499
+ if (Number.isFinite(ms) && ms >= 0) {
500
+ out.durationMs = ms;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ return out;
506
+ }
507
+ function parseTrackTags(window, tags) {
508
+ const bags = new Map();
509
+ for (const tag of E.children(window, tags.body, tags.size)) {
510
+ if (tag.id !== E.ID.tag.ENTRY)
511
+ continue;
512
+ let uid = null;
513
+ const tags = [];
514
+ for (const field of E.children(window, tag.body, tag.size)) {
515
+ if (field.id === E.ID.tag.TARGETS) {
516
+ for (const target of E.children(window, field.body, field.size)) {
517
+ if (target.id === E.ID.tag.TRACK_UID) {
518
+ uid = E.readUid(window, target);
519
+ }
520
+ }
521
+ }
522
+ else if (field.id === E.ID.tag.SIMPLE) {
523
+ tags.push(simpleTag(window, field));
524
+ }
525
+ }
526
+ if (uid === null || tags.length === 0)
527
+ continue;
528
+ bags.set(uid, [...(bags.get(uid) ?? []), ...tags]);
529
+ }
530
+ return bags;
531
+ }
@@ -0,0 +1,57 @@
1
+ import type { Track } from "../tracks/core.js";
2
+ import type { Attachment, ContainerInfo, PeekCode, RefusalCode, Wants } from "../vocabulary.js";
3
+ /** The options every entry point shares. */
4
+ export interface PeekOptions {
5
+ /** Opt in to container-level facts in the envelope. */
6
+ info?: boolean;
7
+ /** Opt in to attachment metadata in the envelope. */
8
+ attachments?: boolean;
9
+ /**
10
+ * The signal that aborts the read.
11
+ *
12
+ * The returned {@linkcode PeekOutcome.code} is `cancelled`.
13
+ *
14
+ * @example
15
+ * const stop = new AbortController();
16
+ * const patience = setTimeout(() => stop.abort(), 5_000);
17
+ * await peekTracks(url, { signal: stop.signal }).finally(() => clearTimeout(patience));
18
+ */
19
+ signal?: AbortSignal;
20
+ }
21
+ /**
22
+ * The answer every read returns.
23
+ *
24
+ * Served or refused, it comes as a value and never as a throw.
25
+ */
26
+ export interface PeekOutcome<T extends Track> extends HeaderAnswer<T> {
27
+ code: PeekCode;
28
+ }
29
+ /** What the header answered, as one piece. */
30
+ interface HeaderAnswer<T extends Track> {
31
+ /** Comes with a refusal too, as long as the header was read. */
32
+ tracks: T[];
33
+ info?: ContainerInfo | null;
34
+ attachments?: Attachment[];
35
+ }
36
+ /** The two opt-in siblings, either what the header read or the absent answer. */
37
+ interface Siblings {
38
+ info: ContainerInfo | null;
39
+ attachments: Attachment[];
40
+ }
41
+ export declare const served: <T extends Track>(answer: HeaderAnswer<T>) => PeekOutcome<T>;
42
+ /**
43
+ * The outcome of a refusal.
44
+ *
45
+ * It carries the answer bought if the header was read, and {@linkcode absent} if it was not.
46
+ */
47
+ export declare const refused: <T extends Track>(code: RefusalCode, answer: HeaderAnswer<T>) => PeekOutcome<T>;
48
+ export declare const wantsOf: (options: PeekOptions) => Wants;
49
+ /**
50
+ * The answer when the header was not read.
51
+ *
52
+ * No tracks can exist, and a requested sibling stands as its absent answer.
53
+ */
54
+ export declare const absent: (wants: Wants) => HeaderAnswer<never>;
55
+ /** An answer carrying only the siblings that were asked for. */
56
+ export declare function answerOf<T extends Track>(read: Siblings, tracks: T[], wants: Wants): HeaderAnswer<T>;
57
+ export {};
@@ -0,0 +1,24 @@
1
+ export const served = (answer) => ({
2
+ ...answer,
3
+ code: "served",
4
+ });
5
+ export const refused = (code, answer) => ({
6
+ ...answer,
7
+ code,
8
+ });
9
+ export const wantsOf = (options) => ({
10
+ info: options.info === true,
11
+ attachments: options.attachments === true,
12
+ });
13
+ export const absent = (wants) => answerOf(ABSENT_SIBLINGS, [], wants);
14
+ export function answerOf(read, tracks, wants) {
15
+ const answer = { tracks };
16
+ if (wants.info) {
17
+ answer.info = read.info;
18
+ }
19
+ if (wants.attachments) {
20
+ answer.attachments = read.attachments;
21
+ }
22
+ return answer;
23
+ }
24
+ const ABSENT_SIBLINGS = { info: null, attachments: [] };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The engine that produces an envelope at header depth.
3
+ *
4
+ * Which kind of answer it becomes is decided by the projection plugged in.
5
+ */
6
+ import { type Source } from "../io/source.js";
7
+ import { type TracksHeader } from "../matroska/header.js";
8
+ import type { Track } from "../tracks/core.js";
9
+ import { type ListedTrack } from "../tracks/list.js";
10
+ import type { RefusalCode } from "../vocabulary.js";
11
+ import { type PeekOptions, type PeekOutcome } from "./contract.js";
12
+ /**
13
+ * Folds what was caught (an abort, a source failure, a grammar error) into a refusal.
14
+ *
15
+ * The caller picks the code for a grammar error; `error` is only asked whether it is one.
16
+ */
17
+ export declare function thrownRefusal(error: unknown, grammarCode: "malformed" | "unreadable", signal: AbortSignal | undefined): RefusalCode;
18
+ export declare function fromHeader<T extends Track>(source: Source, options: PeekOptions, projection: (header: TracksHeader) => T[]): Promise<PeekOutcome<T>>;
19
+ export declare function peekTracksFrom(source: Source, options?: PeekOptions): Promise<PeekOutcome<ListedTrack>>;
@@ -0,0 +1,32 @@
1
+ import * as E from "../ebml.js";
2
+ import { abortReason, stopIfAborted } from "../io/source.js";
3
+ import { readContainer } from "../matroska/header.js";
4
+ import { allTracks } from "../tracks/list.js";
5
+ import { absent, answerOf, refused, served, wantsOf, } from "./contract.js";
6
+ export function thrownRefusal(error, grammarCode, signal) {
7
+ if (abortReason(signal) !== null)
8
+ return "cancelled";
9
+ if (!E.isGrammarError(error))
10
+ return "source-failed";
11
+ return grammarCode;
12
+ }
13
+ export async function fromHeader(source, options, projection) {
14
+ const wants = wantsOf(options);
15
+ const absentAnswer = absent(wants);
16
+ try {
17
+ stopIfAborted(options.signal);
18
+ const readOptions = { ...wants, signal: options.signal };
19
+ const header = await readContainer(source, "tracks", readOptions);
20
+ if (typeof header === "string")
21
+ return refused(header, absentAnswer);
22
+ const projected = projection(header);
23
+ return served(answerOf(header, projected, wants));
24
+ }
25
+ catch (error) {
26
+ const refusal = thrownRefusal(error, "malformed", options.signal);
27
+ return refused(refusal, absentAnswer);
28
+ }
29
+ }
30
+ export async function peekTracksFrom(source, options = {}) {
31
+ return fromHeader(source, options, allTracks);
32
+ }
@@ -0,0 +1,11 @@
1
+ import { type Source } from "../io/source.js";
2
+ import type { Track } from "../tracks/core.js";
3
+ import type { RefusalCode } from "../vocabulary.js";
4
+ import { type PeekOptions, type PeekOutcome } from "./contract.js";
5
+ /** A path (Node) or URL string, bytes in hand, or a {@linkcode Source} opened by the caller. */
6
+ export type Target = Source | ArrayBufferView | ArrayBuffer | string;
7
+ export type Opener = (target: string, signal: AbortSignal | undefined) => Promise<Source | RefusalCode>;
8
+ type Work<T extends Track, O extends PeekOptions> = (source: Source, options: O) => Promise<PeekOutcome<T>>;
9
+ export declare const urlRefusal: (parsed: URL) => RefusalCode | null;
10
+ export declare function overTarget<T extends Track, O extends PeekOptions>(target: Target, options: O, work: Work<T, O>, open?: Opener): Promise<PeekOutcome<T>>;
11
+ export {};