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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 6lvkro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # mkvpeek
2
+
3
+ Looks at the tracks of a Matroska container without demuxing the whole of it.
4
+
5
+ ```text
6
+ Measured on a 2.2 GB file on a local NVMe drive
7
+
8
+ ffprobe -show_streams video.mkv 1.2 MB read 21 ms
9
+ peekTracks("video.mkv") 135 KB read 0.6 ms
10
+
11
+ ffmpeg -i video.mkv -map 0:s:0 -c:s copy out.ass 2.2 GB read 609 ms
12
+ peekSubtitles("video.mkv") 1.6 MB read 60 ms
13
+ ```
14
+
15
+ The two `peek` lines are calls made from a Node process that is already running; the two tools are timed from being spawned as a subprocess to their exit.
16
+
17
+ It performs well when both hold: a low-latency link and the fast path (see [path selection](#constraints-and-read-path-selection)).
18
+
19
+ ## When to use it
20
+
21
+ - You want the track list and the container info without an external demuxer
22
+ - You need to read a container in the browser with no WASM dependency
23
+ - The container sits on a server that allows Range requests
24
+ - You need a subtitle track as a whole, not as a stream of events
25
+ - You want a subtitle track extracted quickly, with fewer reads (or less transfer)
26
+
27
+ Assembling a body is, for now, the subtitle track's alone, and it supports the `ASS` / `SSA` / `SRT` / `VTT` formats.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npm install mkvpeek
33
+ ```
34
+
35
+ Node 22.1 or later, no other dependencies.
36
+
37
+ ## Usage
38
+
39
+ There are two entry points, and a refusal answers with a code instead of throwing.
40
+
41
+ `peekTracks` reads only the header and returns every kind of track; `peekSubtitles` returns subtitle tracks with their bodies.
42
+
43
+ ```ts
44
+ import { peekSubtitles, peekTracks, urlSource } from "mkvpeek";
45
+
46
+ // A file path (Node only)
47
+ await peekTracks("video.mkv");
48
+ await peekSubtitles("video.mkv");
49
+
50
+ // A URL, when the server allows Range requests
51
+ await peekTracks("https://host/video.mkv");
52
+
53
+ // A Uint8Array or an ArrayBuffer
54
+ await peekTracks(bytes);
55
+
56
+ // For options the package cannot predefine, open the source yourself; whoever opens it closes it.
57
+ const source = await urlSource(url, { headers });
58
+ try {
59
+ await peekTracks(source);
60
+ } finally {
61
+ await source.close?.();
62
+ }
63
+ ```
64
+
65
+ If the bytes are already in hand, just pass them.
66
+
67
+ But **reading a file up front only to pass it in throws away the benefit of reading just what is needed.**
68
+ The links where that benefit is worth giving up are discussed at the end of [path selection](#constraints-and-read-path-selection).
69
+
70
+ ```ts
71
+ import { peekSubtitles } from "mkvpeek/browser";
72
+
73
+ // A source that reads a File lazily
74
+ await peekSubtitles({
75
+ size: () => Promise.resolve(file.size),
76
+ read: async (at, n) => new Uint8Array(await file.slice(at, at + n).arrayBuffer()),
77
+ });
78
+ ```
79
+
80
+ In the browser, `mkvpeek/browser` is the same reader minus file paths.
81
+
82
+ ## Constraints and read path selection
83
+
84
+ > The read path does not apply to `peekTracks`, which reads only the header; it applies only when subtitle bodies are involved.
85
+
86
+ Matroska containers usually carry `Cues`, a list of frame positions, but the list can be short or wrong.
87
+ So that a subtitle body the caller asked for is not silently lost when the list falls short,
88
+ the default is to back off the fast path (`index`) and retry along the slow path (`walk`), which visits every cluster.
89
+
90
+ Even with `walk`, though, the range of containers this handles falls short of the external tools.
91
+ If an external tool is available, the recommendation is to use the fast path (`index`) explicitly and hand refused files to that tool.
92
+ On containers where `index` works, both speed and transfer come out well ahead of the binaries, and that alone makes it worth it.
93
+
94
+ ```ts
95
+ import { peekSubtitles, worthFallback } from "mkvpeek";
96
+
97
+ const { code, tracks } = await peekSubtitles(path, { via: "index" });
98
+ if (worthFallback(code)) await demux(path);
99
+ ```
100
+
101
+ On a high-latency network link the slow path (`walk`) loses much of its speed to round trips,
102
+ so accepting the transfer, downloading the whole file once on the consumer side and passing the bytes can be faster.
103
+
104
+ The package does not provide a download path of its own, and it refuses any response to a Range request other than 206.
105
+
106
+ ## Oracle
107
+
108
+ Expected output is verified against ffprobe and ffmpeg, and the deliberate exceptions are, so far, these two.
109
+
110
+ - **When a track does not state its `language`**
111
+ ffprobe emits `eng` as a default; the package emits `null`.
112
+
113
+ - **When a frame in the body does not state its `duration`**
114
+ ffmpeg uses a derived value; the package uses 0.
115
+
116
+ If something the spec defines is distorted or lost in a non-refused return, outside these two, it can be treated as a bug.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,12 @@
1
+ export { peekSubtitles, peekTracks } from "./entry/browser.js";
2
+ export type { Source, Trip } from "./io/source.js";
3
+ export type { UrlOptions } from "./io/url.js";
4
+ export { urlSource } from "./io/url.js";
5
+ export type { PeekOptions, PeekOutcome } from "./peek/contract.js";
6
+ export type { Target } from "./peek/target.js";
7
+ export type { SubtitleOptions, SubtitlePreset, SubtitleProgress, SubtitleTuning, SubtitleVia, } from "./subtitle/options.js";
8
+ export type { Track, TrackType } from "./tracks/core.js";
9
+ export type { ListedTrack } from "./tracks/list.js";
10
+ export type { SubtitleTrack } from "./tracks/subtitle.js";
11
+ export type { Attachment, ContainerInfo, PeekCode, RefusalCode, SubtitleFinder, TrackFlags, TrackTag, } from "./vocabulary.js";
12
+ export { isRefusalCode, worthFallback } from "./vocabulary.js";
@@ -0,0 +1,3 @@
1
+ export { peekSubtitles, peekTracks } from "./entry/browser.js";
2
+ export { urlSource } from "./io/url.js";
3
+ export { isRefusalCode, worthFallback } from "./vocabulary.js";
package/dist/ebml.d.ts ADDED
@@ -0,0 +1,168 @@
1
+ export interface Element {
2
+ id: number;
3
+ size: number;
4
+ body: number;
5
+ next: number;
6
+ }
7
+ /** An element that states no size. */
8
+ export interface OpenElement {
9
+ id: number;
10
+ body: number;
11
+ open: true;
12
+ }
13
+ /**
14
+ * A view of part of the file.
15
+ *
16
+ * Every coordinate is absolute, so it carries where it came from and how far the file goes.
17
+ */
18
+ export interface Window {
19
+ readonly at: number;
20
+ readonly bytes: Uint8Array;
21
+ readonly fileSize: number;
22
+ }
23
+ interface Vint {
24
+ value: number;
25
+ length: number;
26
+ /** The all-ones shape that means the size is unknown. */
27
+ allOnes: boolean;
28
+ }
29
+ /** Nothing could be read at some position. */
30
+ type Unread = "unread";
31
+ /**
32
+ * Thrown where a walk that stopped quietly
33
+ * would turn a damaged list into a short one that looks whole.
34
+ *
35
+ * {@linkcode isGrammarError} tells which it was.
36
+ */
37
+ export declare class GrammarError extends Error {
38
+ }
39
+ export declare const DEFAULT_TIMESTAMP_SCALE = 1000000;
40
+ /**
41
+ * The widest a vint gets.
42
+ *
43
+ * Every header-size constant downstream derives from this number.
44
+ */
45
+ export declare const MAX_VINT_BYTES = 8;
46
+ /**
47
+ * The element ids this reader knows.
48
+ *
49
+ * @see {@link https://www.matroska.org/technical/elements.html}
50
+ */
51
+ export declare const ID: {
52
+ readonly root: {
53
+ readonly EBML: 440786851;
54
+ readonly SEGMENT: 408125543;
55
+ };
56
+ readonly segment: {
57
+ readonly SEEK_HEAD: 290298740;
58
+ readonly INFO: 357149030;
59
+ readonly TRACKS: 374648427;
60
+ readonly TAGS: 307544935;
61
+ readonly CUES: 475249515;
62
+ readonly CLUSTER: 524531317;
63
+ readonly CHAPTERS: 272869232;
64
+ readonly ATTACHMENTS: 423732329;
65
+ };
66
+ readonly seek: {
67
+ readonly ENTRY: 19899;
68
+ readonly ID: 21419;
69
+ readonly POSITION: 21420;
70
+ };
71
+ readonly info: {
72
+ readonly TIMESTAMP_SCALE: 2807729;
73
+ readonly TITLE: 31657;
74
+ readonly MUXING_APP: 19840;
75
+ readonly WRITING_APP: 22337;
76
+ readonly DURATION: 17545;
77
+ };
78
+ readonly track: {
79
+ readonly ENTRY: 174;
80
+ readonly NUMBER: 215;
81
+ readonly UID: 29637;
82
+ readonly TYPE: 131;
83
+ readonly CODEC_ID: 134;
84
+ readonly CODEC_PRIVATE: 25506;
85
+ readonly CODEC_DELAY: 22186;
86
+ readonly LANGUAGE: 2274716;
87
+ readonly LANGUAGE_IETF: 2274717;
88
+ readonly NAME: 21358;
89
+ readonly FLAG_DEFAULT: 136;
90
+ readonly FLAG_FORCED: 21930;
91
+ readonly FLAG_ENABLED: 185;
92
+ readonly FLAG_HEARING_IMPAIRED: 21931;
93
+ readonly FLAG_VISUAL_IMPAIRED: 21932;
94
+ readonly FLAG_TEXT_DESCRIPTIONS: 21933;
95
+ readonly FLAG_ORIGINAL: 21934;
96
+ readonly FLAG_COMMENTARY: 21935;
97
+ readonly CONTENT_ENCODINGS: 28032;
98
+ };
99
+ readonly attachment: {
100
+ readonly FILE: 24999;
101
+ readonly FILE_NAME: 18030;
102
+ readonly FILE_MIME: 18016;
103
+ readonly FILE_DESCRIPTION: 18046;
104
+ readonly FILE_DATA: 18012;
105
+ readonly FILE_UID: 18094;
106
+ };
107
+ readonly tag: {
108
+ readonly ENTRY: 29555;
109
+ readonly TARGETS: 25536;
110
+ readonly TRACK_UID: 25541;
111
+ readonly SIMPLE: 26568;
112
+ readonly NAME: 17827;
113
+ readonly STRING: 17543;
114
+ };
115
+ readonly cue: {
116
+ readonly POINT: 187;
117
+ readonly TRACK_POSITIONS: 183;
118
+ readonly TRACK: 247;
119
+ readonly CLUSTER_POSITION: 241;
120
+ readonly RELATIVE_POSITION: 240;
121
+ };
122
+ readonly cluster: {
123
+ readonly TIMESTAMP: 231;
124
+ readonly SIMPLE_BLOCK: 163;
125
+ readonly BLOCK_GROUP: 160;
126
+ readonly POSITION: 167;
127
+ readonly PREV_SIZE: 171;
128
+ readonly SILENT_TRACKS: 22612;
129
+ readonly ENCRYPTED_BLOCK: 175;
130
+ };
131
+ readonly blockGroup: {
132
+ readonly BLOCK: 161;
133
+ readonly BLOCK_DURATION: 155;
134
+ };
135
+ readonly global: {
136
+ readonly CRC32: 191;
137
+ readonly VOID: 236;
138
+ };
139
+ };
140
+ /** Decoded as a sized element. */
141
+ export declare const decoded: (el: Element | OpenElement | Unread) => el is Element;
142
+ /** Decoded as a sized element that ends within its parent. */
143
+ export declare const fits: (el: Element | OpenElement | Unread, end: number) => el is Element;
144
+ export declare const covers: (window: Window, at: number, length: number) => boolean;
145
+ export declare const reachOf: (window: Window) => number;
146
+ export declare const isGrammarError: (error: unknown) => boolean;
147
+ export declare const decodeUtf8: (bytes: Uint8Array) => string;
148
+ export declare const uintFits: (size: number) => boolean;
149
+ export declare function readVint(window: Window, at: number, as: "id" | "size"): Vint | Unread;
150
+ export declare function vintLength(lead: number): number;
151
+ /**
152
+ * The value of a vint whose length is known.
153
+ *
154
+ * `id` keeps the marker bit so the value compares to the constants as is; `size` drops it.
155
+ */
156
+ export declare function vintValue(bytes: Uint8Array, start: number, length: number, as: "id" | "size"): number;
157
+ /** The all-ones shape of a size vint, which means the size is unknown. */
158
+ export declare function vintAllOnes(bytes: Uint8Array, start: number, length: number): boolean;
159
+ export declare function uintAt(bytes: Uint8Array, start: number, size: number): number;
160
+ export declare function readElement(window: Window, at: number): Element | OpenElement | Unread;
161
+ export declare function children(window: Window, body: number, size: number): Generator<Element>;
162
+ export declare function readFloat(window: Window, el: Element): number | null;
163
+ export declare function readUint(window: Window, el: Element): number | null;
164
+ export declare function readUid(window: Window, el: Element): string | null;
165
+ export declare function readAscii(window: Window, el: Element): string;
166
+ export declare function readUtf8(window: Window, el: Element): string;
167
+ export declare function readBytes(window: Window, at: number, length: number): Uint8Array;
168
+ export {};
package/dist/ebml.js ADDED
@@ -0,0 +1,224 @@
1
+ export class GrammarError extends Error {
2
+ }
3
+ export const DEFAULT_TIMESTAMP_SCALE = 1_000_000;
4
+ export const MAX_VINT_BYTES = 8;
5
+ export const ID = {
6
+ root: {
7
+ EBML: 0x1a45dfa3,
8
+ SEGMENT: 0x18538067,
9
+ },
10
+ segment: {
11
+ SEEK_HEAD: 0x114d9b74,
12
+ INFO: 0x1549a966,
13
+ TRACKS: 0x1654ae6b,
14
+ TAGS: 0x1254c367,
15
+ CUES: 0x1c53bb6b,
16
+ CLUSTER: 0x1f43b675,
17
+ CHAPTERS: 0x1043a770,
18
+ ATTACHMENTS: 0x1941a469,
19
+ },
20
+ seek: {
21
+ ENTRY: 0x4dbb,
22
+ ID: 0x53ab,
23
+ POSITION: 0x53ac,
24
+ },
25
+ info: {
26
+ TIMESTAMP_SCALE: 0x2ad7b1,
27
+ TITLE: 0x7ba9,
28
+ MUXING_APP: 0x4d80,
29
+ WRITING_APP: 0x5741,
30
+ DURATION: 0x4489,
31
+ },
32
+ track: {
33
+ ENTRY: 0xae,
34
+ NUMBER: 0xd7,
35
+ UID: 0x73c5,
36
+ TYPE: 0x83,
37
+ CODEC_ID: 0x86,
38
+ CODEC_PRIVATE: 0x63a2,
39
+ CODEC_DELAY: 0x56aa,
40
+ LANGUAGE: 0x22b59c,
41
+ LANGUAGE_IETF: 0x22b59d,
42
+ NAME: 0x536e,
43
+ FLAG_DEFAULT: 0x88,
44
+ FLAG_FORCED: 0x55aa,
45
+ FLAG_ENABLED: 0xb9,
46
+ FLAG_HEARING_IMPAIRED: 0x55ab,
47
+ FLAG_VISUAL_IMPAIRED: 0x55ac,
48
+ FLAG_TEXT_DESCRIPTIONS: 0x55ad,
49
+ FLAG_ORIGINAL: 0x55ae,
50
+ FLAG_COMMENTARY: 0x55af,
51
+ CONTENT_ENCODINGS: 0x6d80,
52
+ },
53
+ attachment: {
54
+ FILE: 0x61a7,
55
+ FILE_NAME: 0x466e,
56
+ FILE_MIME: 0x4660,
57
+ FILE_DESCRIPTION: 0x467e,
58
+ FILE_DATA: 0x465c,
59
+ FILE_UID: 0x46ae,
60
+ },
61
+ tag: {
62
+ ENTRY: 0x7373,
63
+ TARGETS: 0x63c0,
64
+ TRACK_UID: 0x63c5,
65
+ SIMPLE: 0x67c8,
66
+ NAME: 0x45a3,
67
+ STRING: 0x4487,
68
+ },
69
+ cue: {
70
+ POINT: 0xbb,
71
+ TRACK_POSITIONS: 0xb7,
72
+ TRACK: 0xf7,
73
+ CLUSTER_POSITION: 0xf1,
74
+ RELATIVE_POSITION: 0xf0,
75
+ },
76
+ cluster: {
77
+ TIMESTAMP: 0xe7,
78
+ SIMPLE_BLOCK: 0xa3,
79
+ BLOCK_GROUP: 0xa0,
80
+ POSITION: 0xa7,
81
+ PREV_SIZE: 0xab,
82
+ SILENT_TRACKS: 0x5854,
83
+ ENCRYPTED_BLOCK: 0xaf,
84
+ },
85
+ blockGroup: {
86
+ BLOCK: 0xa1,
87
+ BLOCK_DURATION: 0x9b,
88
+ },
89
+ global: {
90
+ CRC32: 0xbf,
91
+ VOID: 0xec,
92
+ },
93
+ };
94
+ export const decoded = (el) => typeof el !== "string" && !("open" in el);
95
+ export const fits = (el, end) => decoded(el) && el.next <= end;
96
+ export const covers = (window, at, length) => offsetOf(window, at, length) >= 0;
97
+ export const reachOf = (window) => window.at + window.bytes.length;
98
+ export const isGrammarError = (error) => error instanceof GrammarError;
99
+ export const decodeUtf8 = (bytes) => UTF8.decode(bytes);
100
+ export const uintFits = (size) => size <= MAX_VINT_BYTES;
101
+ export function readVint(window, at, as) {
102
+ const start = offsetOf(window, at, 1);
103
+ if (start < 0)
104
+ return "unread";
105
+ const length = vintLength(window.bytes[start]);
106
+ if (length > MAX_VINT_BYTES)
107
+ return "unread";
108
+ if (offsetOf(window, at, length) < 0)
109
+ return "unread";
110
+ const value = vintValue(window.bytes, start, length, as);
111
+ const allOnes = vintAllOnes(window.bytes, start, length);
112
+ return { value, length, allOnes };
113
+ }
114
+ export function vintLength(lead) {
115
+ let length = 1;
116
+ while (length <= MAX_VINT_BYTES && (lead & (0x80 >> (length - 1))) === 0) {
117
+ length += 1;
118
+ }
119
+ return length;
120
+ }
121
+ export function vintValue(bytes, start, length, as) {
122
+ const lead = bytes[start];
123
+ let value = as === "id" ? lead : lead & (ALL_BITS >> length);
124
+ for (let i = 1; i < length; i++) {
125
+ value = value * 256 + bytes[start + i];
126
+ }
127
+ return value;
128
+ }
129
+ export function vintAllOnes(bytes, start, length) {
130
+ const leadValueBits = ALL_BITS >> length;
131
+ if ((bytes[start] & leadValueBits) !== leadValueBits)
132
+ return false;
133
+ for (let i = 1; i < length; i++) {
134
+ if (bytes[start + i] !== ALL_BITS)
135
+ return false;
136
+ }
137
+ return true;
138
+ }
139
+ export function uintAt(bytes, start, size) {
140
+ let value = 0;
141
+ for (let i = 0; i < size; i++) {
142
+ value = value * 256 + bytes[start + i];
143
+ }
144
+ return value;
145
+ }
146
+ export function readElement(window, at) {
147
+ const { fileSize } = window;
148
+ const id = readVint(window, at, "id");
149
+ if (typeof id === "string")
150
+ return id;
151
+ const size = readVint(window, at + id.length, "size");
152
+ if (typeof size === "string")
153
+ return size;
154
+ const body = at + id.length + size.length;
155
+ if (size.allOnes) {
156
+ if (id.value !== ID.root.SEGMENT && id.value !== ID.segment.CLUSTER)
157
+ return "unread";
158
+ return { id: id.value, body, open: true };
159
+ }
160
+ if (body + size.value > fileSize)
161
+ return "unread";
162
+ return { id: id.value, size: size.value, body, next: body + size.value };
163
+ }
164
+ export function* children(window, body, size) {
165
+ let at = body;
166
+ const end = body + size;
167
+ while (at < end) {
168
+ const el = readElement(window, at);
169
+ const invalidGrammar = typeof el === "string" || "open" in el || el.next > end;
170
+ if (invalidGrammar)
171
+ throw new GrammarError();
172
+ yield el;
173
+ at = el.next;
174
+ }
175
+ }
176
+ export function readFloat(window, el) {
177
+ const bytes = readBytes(window, el.body, el.size);
178
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
179
+ if (el.size === 4)
180
+ return view.getFloat32(0);
181
+ if (el.size === 8)
182
+ return view.getFloat64(0);
183
+ return null;
184
+ }
185
+ export function readUint(window, el) {
186
+ if (!uintFits(el.size))
187
+ return null;
188
+ const start = offsetOf(window, el.body, el.size);
189
+ if (start < 0)
190
+ throw new GrammarError();
191
+ return uintAt(window.bytes, start, el.size);
192
+ }
193
+ export function readUid(window, el) {
194
+ let hex = "";
195
+ for (const byte of readBytes(window, el.body, el.size)) {
196
+ hex += byte.toString(16).padStart(2, "0");
197
+ }
198
+ const trimmed = hex.replace(/^0+/, "");
199
+ return trimmed === "" ? null : trimmed;
200
+ }
201
+ export function readAscii(window, el) {
202
+ let out = "";
203
+ for (const byte of readBytes(window, el.body, el.size)) {
204
+ out += String.fromCharCode(byte);
205
+ }
206
+ return out.replace(/\0+$/, "");
207
+ }
208
+ export function readUtf8(window, el) {
209
+ return decodeUtf8(readBytes(window, el.body, el.size));
210
+ }
211
+ export function readBytes(window, at, length) {
212
+ const start = offsetOf(window, at, length);
213
+ if (start < 0) {
214
+ throw new GrammarError();
215
+ }
216
+ return window.bytes.subarray(start, start + length);
217
+ }
218
+ const ALL_BITS = 0xff;
219
+ const KEEPS_BOM = { ignoreBOM: true };
220
+ const UTF8 = new TextDecoder("utf-8", KEEPS_BOM);
221
+ function offsetOf(window, at, length) {
222
+ const start = at - window.at;
223
+ return start < 0 || start + length > window.bytes.length ? -1 : start;
224
+ }
@@ -0,0 +1,44 @@
1
+ import type { PeekOptions, PeekOutcome } from "../peek/contract.js";
2
+ import { type Target } from "../peek/target.js";
3
+ import type { SubtitleOptions } from "../subtitle/options.js";
4
+ import type { ListedTrack } from "../tracks/list.js";
5
+ import type { SubtitleTrack } from "../tracks/subtitle.js";
6
+ /**
7
+ * Peeks at a container's subtitles.
8
+ *
9
+ * @example
10
+ * // Bytes already in memory (an upload body, a queue message)
11
+ * const { code, tracks } = await peekSubtitles(bytes);
12
+ *
13
+ * @example
14
+ * // A URL, when the server allows Range requests
15
+ * const { code, tracks } = await peekSubtitles(url);
16
+ *
17
+ * @example
18
+ * // Options the package cannot predefine: open the source yourself, and close what you opened.
19
+ * const source = await urlSource(url, { headers });
20
+ * try {
21
+ * const { code, tracks } = await peekSubtitles(source);
22
+ * } finally {
23
+ * await source.close?.();
24
+ * }
25
+ *
26
+ * @example
27
+ * // Without loading an uploaded File into memory
28
+ * const source: Source = {
29
+ * size: () => Promise.resolve(file.size),
30
+ * read: async (at, n) => new Uint8Array(await file.slice(at, at + n).arrayBuffer()),
31
+ * };
32
+ * const { code, tracks } = await peekSubtitles(source);
33
+ */
34
+ export declare function peekSubtitles(target: Target, options?: SubtitleOptions): Promise<PeekOutcome<SubtitleTrack>>;
35
+ /**
36
+ * Peeks at a container's track list
37
+ *
38
+ * Track-level facts only, every kind flat in one array.
39
+ *
40
+ * @example
41
+ * const { tracks } = await peekTracks(url);
42
+ * const dubbed = tracks.filter((t) => t.type === "audio" && t.language !== null);
43
+ */
44
+ export declare function peekTracks(target: Target, options?: PeekOptions): Promise<PeekOutcome<ListedTrack>>;
@@ -0,0 +1,9 @@
1
+ import { peekTracksFrom } from "../peek/engine.js";
2
+ import { overTarget } from "../peek/target.js";
3
+ import { peekSubtitlesFrom } from "../subtitle/peek.js";
4
+ export function peekSubtitles(target, options = {}) {
5
+ return overTarget(target, options, peekSubtitlesFrom);
6
+ }
7
+ export function peekTracks(target, options = {}) {
8
+ return overTarget(target, options, peekTracksFrom);
9
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The entry point that also takes file paths.
3
+ *
4
+ * The browser bundle takes only URLs and bytes, on a surface of the same names.
5
+ */
6
+ import type { PeekOptions, PeekOutcome } from "../peek/contract.js";
7
+ import { type Target } from "../peek/target.js";
8
+ import type { SubtitleOptions } from "../subtitle/options.js";
9
+ import type { ListedTrack } from "../tracks/list.js";
10
+ import type { SubtitleTrack } from "../tracks/subtitle.js";
11
+ /**
12
+ * Peeks at a container's subtitles.
13
+ *
14
+ * For examples beyond file paths, see the same name in `mkvpeek/browser`.
15
+ *
16
+ * @example
17
+ * const { code, tracks } = await peekSubtitles(path);
18
+ *
19
+ * @example
20
+ * // The list alone, one read of the head and no bodies. Bodies cost the index or a walk.
21
+ * const { code, tracks } = await peekSubtitles(path, { text: false });
22
+ */
23
+ export declare function peekSubtitles(target: Target, options?: SubtitleOptions): Promise<PeekOutcome<SubtitleTrack>>;
24
+ /**
25
+ * Peeks at a container's track list
26
+ *
27
+ * Track-level facts only, every kind flat in one array.
28
+ *
29
+ * For examples beyond file paths, see the same name in `mkvpeek/browser`.
30
+ *
31
+ * @example
32
+ * const { tracks, info, attachments } = await peekTracks(path, {
33
+ * info: true,
34
+ * attachments: true,
35
+ * });
36
+ */
37
+ export declare function peekTracks(target: Target, options?: PeekOptions): Promise<PeekOutcome<ListedTrack>>;
@@ -0,0 +1,38 @@
1
+ import { fileSource } from "../io/file.js";
2
+ import { isHttpUrl, namesAuthority } from "../io/url.js";
3
+ import { urlSource } from "../io/url-node.js";
4
+ import { peekTracksFrom } from "../peek/engine.js";
5
+ import { overTarget, urlRefusal } from "../peek/target.js";
6
+ import { peekSubtitlesFrom } from "../subtitle/peek.js";
7
+ export function peekSubtitles(target, options = {}) {
8
+ return overTarget(target, options, peekSubtitlesFrom, viaPath);
9
+ }
10
+ export function peekTracks(target, options = {}) {
11
+ return overTarget(target, options, peekTracksFrom, viaPath);
12
+ }
13
+ const viaPath = async (target, signal) => {
14
+ if (isHttpUrl(target))
15
+ return urlSource(target, { signal });
16
+ if (namesAuthority(target)) {
17
+ const parsed = URL.parse(target);
18
+ if (parsed !== null) {
19
+ const refusal = urlRefusal(parsed);
20
+ if (refusal !== null)
21
+ return refusal;
22
+ }
23
+ }
24
+ try {
25
+ return await fileSource(target);
26
+ }
27
+ catch (error) {
28
+ const parsed = URL.parse(target);
29
+ const intendedAsUrl = parsed !== null &&
30
+ (parsed.protocol === "file:" || parsed.search !== "" || parsed.hash !== "");
31
+ if (intendedAsUrl) {
32
+ const refusal = urlRefusal(parsed);
33
+ if (refusal !== null)
34
+ return refusal;
35
+ }
36
+ throw error;
37
+ }
38
+ };