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.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.js +3 -0
- package/dist/ebml.d.ts +168 -0
- package/dist/ebml.js +224 -0
- package/dist/entry/browser.d.ts +44 -0
- package/dist/entry/browser.js +9 -0
- package/dist/entry/node.d.ts +37 -0
- package/dist/entry/node.js +38 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +3 -0
- package/dist/io/file.d.ts +2 -0
- package/dist/io/file.js +47 -0
- package/dist/io/lanes.d.ts +17 -0
- package/dist/io/lanes.js +124 -0
- package/dist/io/memory.d.ts +2 -0
- package/dist/io/memory.js +16 -0
- package/dist/io/range.d.ts +22 -0
- package/dist/io/range.js +190 -0
- package/dist/io/source.d.ts +102 -0
- package/dist/io/source.js +31 -0
- package/dist/io/url-node.d.ts +10 -0
- package/dist/io/url-node.js +99 -0
- package/dist/io/url.d.ts +21 -0
- package/dist/io/url.js +80 -0
- package/dist/io/windows.d.ts +37 -0
- package/dist/io/windows.js +105 -0
- package/dist/matroska/block.d.ts +47 -0
- package/dist/matroska/block.js +114 -0
- package/dist/matroska/chain.d.ts +55 -0
- package/dist/matroska/chain.js +47 -0
- package/dist/matroska/clusters.d.ts +4 -0
- package/dist/matroska/clusters.js +232 -0
- package/dist/matroska/cues.d.ts +23 -0
- package/dist/matroska/cues.js +93 -0
- package/dist/matroska/header.d.ts +52 -0
- package/dist/matroska/header.js +531 -0
- package/dist/peek/contract.d.ts +57 -0
- package/dist/peek/contract.js +24 -0
- package/dist/peek/engine.d.ts +19 -0
- package/dist/peek/engine.js +32 -0
- package/dist/peek/target.d.ts +11 -0
- package/dist/peek/target.js +56 -0
- package/dist/subtitle/assemble.d.ts +21 -0
- package/dist/subtitle/assemble.js +114 -0
- package/dist/subtitle/conclude.d.ts +20 -0
- package/dist/subtitle/conclude.js +57 -0
- package/dist/subtitle/indexed.d.ts +5 -0
- package/dist/subtitle/indexed.js +172 -0
- package/dist/subtitle/options.d.ts +76 -0
- package/dist/subtitle/options.js +1 -0
- package/dist/subtitle/peek.d.ts +5 -0
- package/dist/subtitle/peek.js +107 -0
- package/dist/subtitle/tuning.d.ts +4 -0
- package/dist/subtitle/tuning.js +76 -0
- package/dist/subtitle/walked.d.ts +5 -0
- package/dist/subtitle/walked.js +28 -0
- package/dist/tracks/core.d.ts +25 -0
- package/dist/tracks/core.js +24 -0
- package/dist/tracks/list.d.ts +8 -0
- package/dist/tracks/list.js +6 -0
- package/dist/tracks/subtitle.d.ts +45 -0
- package/dist/tracks/subtitle.js +38 -0
- package/dist/vocabulary.d.ts +88 -0
- package/dist/vocabulary.js +10 -0
- package/package.json +68 -0
package/dist/io/url.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** URL policy and the fetch transport. */
|
|
2
|
+
import { type RangeOptions } from "./range.js";
|
|
3
|
+
import type { Source } from "./source.js";
|
|
4
|
+
export interface UrlOptions extends RangeOptions {
|
|
5
|
+
headers?: Readonly<Record<string, string>>;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare const TRANSPORT_OWNED_HEADERS: ReadonlySet<string>;
|
|
9
|
+
export declare const whyCredentialed: (parsed: URL) => string | null;
|
|
10
|
+
export declare const namesAuthority: (target: string) => boolean;
|
|
11
|
+
export declare function withoutHeaders(headers: Readonly<Record<string, string>>, names: ReadonlySet<string>): Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* An HTTP source over `fetch`.
|
|
14
|
+
*
|
|
15
|
+
* A server answering a Range request with anything but 206 is not read;
|
|
16
|
+
* the read throws, which the envelope reports as `source-failed`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function urlSource(url: string, options?: UrlOptions): Promise<Source>;
|
|
19
|
+
export declare function isHttpUrl(target: string): boolean;
|
|
20
|
+
export declare function httpTarget(url: string): URL;
|
|
21
|
+
export declare function whyInadmissible(parsed: URL): string | null;
|
package/dist/io/url.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { bytesRange, rangeSource } from "./range.js";
|
|
2
|
+
export const TRANSPORT_OWNED_HEADERS = new Set([
|
|
3
|
+
"host",
|
|
4
|
+
"accept-encoding",
|
|
5
|
+
"range",
|
|
6
|
+
]);
|
|
7
|
+
export const whyCredentialed = (parsed) => parsed.username !== "" || parsed.password !== ""
|
|
8
|
+
? "carries credentials in its URL, and this reader takes them as headers"
|
|
9
|
+
: null;
|
|
10
|
+
export const namesAuthority = (target) => /^[a-z][a-z0-9+.-]*:\/\//i.test(target);
|
|
11
|
+
export function withoutHeaders(headers, names) {
|
|
12
|
+
const kept = Object.entries(headers).filter(([name]) => !names.has(name.toLowerCase()));
|
|
13
|
+
return Object.fromEntries(kept);
|
|
14
|
+
}
|
|
15
|
+
export async function urlSource(url, options = {}) {
|
|
16
|
+
const transport = fetchTransport(url, options);
|
|
17
|
+
return rangeSource(url, transport, options);
|
|
18
|
+
}
|
|
19
|
+
export function isHttpUrl(target) {
|
|
20
|
+
const parsed = URL.parse(target);
|
|
21
|
+
return parsed !== null && speaksHttp(parsed);
|
|
22
|
+
}
|
|
23
|
+
export function httpTarget(url) {
|
|
24
|
+
const parsed = URL.parse(url);
|
|
25
|
+
if (parsed === null)
|
|
26
|
+
throw new Error(`${url} is not a URL`);
|
|
27
|
+
const why = whyInadmissible(parsed);
|
|
28
|
+
if (why !== null)
|
|
29
|
+
throw new Error(`${url} ${why}`);
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
export function whyInadmissible(parsed) {
|
|
33
|
+
const secret = whyCredentialed(parsed);
|
|
34
|
+
if (secret !== null)
|
|
35
|
+
return secret;
|
|
36
|
+
if (!speaksHttp(parsed))
|
|
37
|
+
return `names ${parsed.protocol}, and this reader speaks http(s)`;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
async function* chunksOf(reader) {
|
|
41
|
+
if (reader === undefined)
|
|
42
|
+
return;
|
|
43
|
+
for (;;) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done)
|
|
46
|
+
return;
|
|
47
|
+
yield value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function fetchTransport(url, options) {
|
|
51
|
+
const { headers = {}, fetch: fetchImpl = globalThis.fetch } = options;
|
|
52
|
+
httpTarget(url);
|
|
53
|
+
const withRange = (from, to) => {
|
|
54
|
+
const out = new Headers(withoutHeaders(headers, TRANSPORT_OWNED_HEADERS));
|
|
55
|
+
const range = bytesRange(from, to);
|
|
56
|
+
out.set("Range", range);
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
return async (from, to, signal) => {
|
|
60
|
+
const init = {
|
|
61
|
+
...(signal === undefined ? {} : { signal }),
|
|
62
|
+
headers: withRange(from, to),
|
|
63
|
+
};
|
|
64
|
+
const response = await fetchImpl(url, init);
|
|
65
|
+
const reader = response.body?.getReader();
|
|
66
|
+
const cancel = async () => {
|
|
67
|
+
const releasing = reader === undefined ? response.body?.cancel() : reader.cancel();
|
|
68
|
+
await releasing?.catch?.(() => { });
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
status: response.status,
|
|
72
|
+
header: (name) => response.headers.get(name),
|
|
73
|
+
body: chunksOf(reader),
|
|
74
|
+
cancel,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function speaksHttp(parsed) {
|
|
79
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
80
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plumbing of windows.
|
|
3
|
+
*
|
|
4
|
+
* Makes windows from a source, indexes them, and reuses them.
|
|
5
|
+
*/
|
|
6
|
+
import type { Window } from "../ebml.js";
|
|
7
|
+
import { type IndexFetch, type Range, type SizedSource, type Source } from "./source.js";
|
|
8
|
+
/** The window buffers the walk reuses. */
|
|
9
|
+
export interface WindowPool {
|
|
10
|
+
/**
|
|
11
|
+
* A lease when the width fits the threshold and a slot.
|
|
12
|
+
*
|
|
13
|
+
* Otherwise null, which is the fresh-allocation path.
|
|
14
|
+
*/
|
|
15
|
+
borrow(width: number): Lease | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The lease of one slot.
|
|
19
|
+
*
|
|
20
|
+
* `release` is one-shot; a second call throws.
|
|
21
|
+
*/
|
|
22
|
+
interface Lease {
|
|
23
|
+
readonly bytes: Uint8Array;
|
|
24
|
+
release: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare class WindowIndex {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(windows: readonly Window[], head?: Window);
|
|
29
|
+
find(at: number, length: number): Window | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function fetchRanges(sized: SizedSource, ranges: readonly Range[], options: Pick<IndexFetch, "gapBytes" | "concurrency">, signal?: AbortSignal): Promise<Window[]>;
|
|
32
|
+
export declare function fetchSpans(sized: SizedSource, spans: readonly Range[], concurrency: number, signal?: AbortSignal): Promise<Window[]>;
|
|
33
|
+
export declare function coalesce(ranges: readonly Range[], options: Pick<IndexFetch, "gapBytes">): Range[];
|
|
34
|
+
export declare function windowPool(slotBytes: number, slots: number): WindowPool;
|
|
35
|
+
export declare function windowOver(source: Source, at: number, length: number, held: Window): Promise<Window>;
|
|
36
|
+
export declare function windowAt({ source, fileSize }: SizedSource, at: number, length: number, into?: Uint8Array): Promise<Window>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as E from "../ebml.js";
|
|
2
|
+
import { inParallel } from "./lanes.js";
|
|
3
|
+
import { available, LARGE_READ_BYTES, MAX_SPAN_BYTES, } from "./source.js";
|
|
4
|
+
export class WindowIndex {
|
|
5
|
+
#sorted;
|
|
6
|
+
#widestSorted;
|
|
7
|
+
#head;
|
|
8
|
+
constructor(windows, head) {
|
|
9
|
+
this.#sorted = [...windows].sort((a, b) => a.at - b.at);
|
|
10
|
+
this.#widestSorted = this.#sorted.reduce((max, w) => Math.max(max, w.bytes.length), 0);
|
|
11
|
+
this.#head = head;
|
|
12
|
+
}
|
|
13
|
+
find(at, length) {
|
|
14
|
+
let low = 0;
|
|
15
|
+
let high = this.#sorted.length;
|
|
16
|
+
while (low < high) {
|
|
17
|
+
const mid = (low + high) >>> 1;
|
|
18
|
+
if (this.#sorted[mid].at <= at) {
|
|
19
|
+
low = mid + 1;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
high = mid;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
let best = this.#head !== undefined && E.covers(this.#head, at, length) ? this.#head : null;
|
|
26
|
+
for (let i = low - 1; i >= 0; i--) {
|
|
27
|
+
const window = this.#sorted[i];
|
|
28
|
+
if (at - window.at > this.#widestSorted)
|
|
29
|
+
break;
|
|
30
|
+
if (!E.covers(window, at, length))
|
|
31
|
+
continue;
|
|
32
|
+
if (best === null || E.reachOf(window) > E.reachOf(best)) {
|
|
33
|
+
best = window;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return best;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function fetchRanges(sized, ranges, options, signal) {
|
|
40
|
+
const spans = coalesce(ranges, options);
|
|
41
|
+
return fetchSpans(sized, spans, options.concurrency, signal);
|
|
42
|
+
}
|
|
43
|
+
export async function fetchSpans(sized, spans, concurrency, signal) {
|
|
44
|
+
const windowFor = (span) => windowAt(sized, span.at, span.length);
|
|
45
|
+
return inParallel(spans, concurrency, windowFor, signal);
|
|
46
|
+
}
|
|
47
|
+
export function coalesce(ranges, options) {
|
|
48
|
+
const sorted = [...ranges].filter((r) => r.length > 0).sort((a, b) => a.at - b.at);
|
|
49
|
+
const spans = [];
|
|
50
|
+
for (const range of sorted) {
|
|
51
|
+
const last = spans[spans.length - 1];
|
|
52
|
+
if (last === undefined) {
|
|
53
|
+
spans.push({ ...range });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const lastEnd = last.at + last.length;
|
|
57
|
+
const merged = Math.max(lastEnd, range.at + range.length) - last.at;
|
|
58
|
+
if (range.at - lastEnd <= options.gapBytes && merged <= MAX_SPAN_BYTES) {
|
|
59
|
+
last.length = merged;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
spans.push({ ...range });
|
|
63
|
+
}
|
|
64
|
+
return spans;
|
|
65
|
+
}
|
|
66
|
+
export function windowPool(slotBytes, slots) {
|
|
67
|
+
const free = [];
|
|
68
|
+
let unbuilt = slotBytes >= LARGE_READ_BYTES ? slots : 0;
|
|
69
|
+
const lease = (slot, width) => {
|
|
70
|
+
let released = false;
|
|
71
|
+
const release = () => {
|
|
72
|
+
if (released)
|
|
73
|
+
throw new Error();
|
|
74
|
+
released = true;
|
|
75
|
+
free.push(slot);
|
|
76
|
+
};
|
|
77
|
+
return { bytes: slot.subarray(0, width), release: release };
|
|
78
|
+
};
|
|
79
|
+
const borrow = (width) => {
|
|
80
|
+
if (width < LARGE_READ_BYTES || width > slotBytes)
|
|
81
|
+
return null;
|
|
82
|
+
let slot = free.pop() ?? null;
|
|
83
|
+
if (slot === null && unbuilt > 0) {
|
|
84
|
+
unbuilt -= 1;
|
|
85
|
+
const owned = new ArrayBuffer(slotBytes);
|
|
86
|
+
slot = new Uint8Array(owned);
|
|
87
|
+
}
|
|
88
|
+
return slot === null ? null : lease(slot, width);
|
|
89
|
+
};
|
|
90
|
+
return { borrow };
|
|
91
|
+
}
|
|
92
|
+
export async function windowOver(source, at, length, held) {
|
|
93
|
+
const need = available(at, length, held.fileSize);
|
|
94
|
+
if (E.covers(held, at, need))
|
|
95
|
+
return held;
|
|
96
|
+
const heldSource = { source, fileSize: held.fileSize };
|
|
97
|
+
return windowAt(heldSource, at, length);
|
|
98
|
+
}
|
|
99
|
+
export async function windowAt({ source, fileSize }, at, length, into) {
|
|
100
|
+
const bytes = await source.read(at, length, into);
|
|
101
|
+
const expected = available(at, length, fileSize);
|
|
102
|
+
if (bytes.length < expected)
|
|
103
|
+
throw new Error();
|
|
104
|
+
return { at, bytes, fileSize };
|
|
105
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Element, Window } from "../ebml.js";
|
|
2
|
+
export interface Frame {
|
|
3
|
+
readOrder: number | null;
|
|
4
|
+
startTicks: number;
|
|
5
|
+
durationTicks: number | null;
|
|
6
|
+
payload: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The hand that supplies bytes.
|
|
10
|
+
*
|
|
11
|
+
* The walk buys them, the index finds them in held windows; failing that, a GrammarError.
|
|
12
|
+
*/
|
|
13
|
+
export type BytesSupply = (at: number, need: number) => Promise<Window>;
|
|
14
|
+
/** For a claimed track, whether it is numbered; otherwise null. */
|
|
15
|
+
export type TrackClaim = (track: number) => boolean | null;
|
|
16
|
+
interface ClaimedFrame {
|
|
17
|
+
track: number;
|
|
18
|
+
frame: Frame;
|
|
19
|
+
}
|
|
20
|
+
/** The definitions that name which rule stood at a refusal. */
|
|
21
|
+
export declare const BLOCK_REFUSAL: {
|
|
22
|
+
readonly notThisTrack: "not-this-track";
|
|
23
|
+
readonly notACluster: "not-a-cluster";
|
|
24
|
+
readonly openCluster: "open-cluster";
|
|
25
|
+
readonly noClusterTime: "no-cluster-time";
|
|
26
|
+
readonly runsPastCluster: "runs-past-cluster";
|
|
27
|
+
readonly notAnElement: "not-an-element";
|
|
28
|
+
readonly pastTheCluster: "past-the-cluster";
|
|
29
|
+
readonly notABlock: "not-a-block";
|
|
30
|
+
readonly notFetched: "not-fetched";
|
|
31
|
+
readonly groupChildren: "group-children";
|
|
32
|
+
readonly groupEmpty: "group-empty";
|
|
33
|
+
readonly strayBlock: "stray-block";
|
|
34
|
+
readonly stranger: "stranger";
|
|
35
|
+
readonly blockHead: "block-head";
|
|
36
|
+
readonly laced: "laced";
|
|
37
|
+
readonly shortHead: "short-head";
|
|
38
|
+
};
|
|
39
|
+
export type BlockClause = (typeof BLOCK_REFUSAL)[keyof typeof BLOCK_REFUSAL];
|
|
40
|
+
export declare const BLOCK_HEAD_BYTES = 32;
|
|
41
|
+
/**
|
|
42
|
+
* One block element as a frame.
|
|
43
|
+
*
|
|
44
|
+
* An unclaimed track is null, and a rule that stands is its clause.
|
|
45
|
+
*/
|
|
46
|
+
export declare function frameIn(outer: Element, clusterTicks: number, held: Window, claim: TrackClaim, supply: BytesSupply): Promise<ClaimedFrame | null | BlockClause>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as E from "../ebml.js";
|
|
2
|
+
import { headerNeed } from "./chain.js";
|
|
3
|
+
export const BLOCK_REFUSAL = {
|
|
4
|
+
notThisTrack: "not-this-track",
|
|
5
|
+
notACluster: "not-a-cluster",
|
|
6
|
+
openCluster: "open-cluster",
|
|
7
|
+
noClusterTime: "no-cluster-time",
|
|
8
|
+
runsPastCluster: "runs-past-cluster",
|
|
9
|
+
notAnElement: "not-an-element",
|
|
10
|
+
pastTheCluster: "past-the-cluster",
|
|
11
|
+
notABlock: "not-a-block",
|
|
12
|
+
notFetched: "not-fetched",
|
|
13
|
+
groupChildren: "group-children",
|
|
14
|
+
groupEmpty: "group-empty",
|
|
15
|
+
strayBlock: "stray-block",
|
|
16
|
+
stranger: "stranger",
|
|
17
|
+
blockHead: "block-head",
|
|
18
|
+
laced: "laced",
|
|
19
|
+
shortHead: "short-head",
|
|
20
|
+
};
|
|
21
|
+
export const BLOCK_HEAD_BYTES = 32;
|
|
22
|
+
export async function frameIn(outer, clusterTicks, held, claim, supply) {
|
|
23
|
+
let window = held;
|
|
24
|
+
let body = outer.body;
|
|
25
|
+
let size = outer.size;
|
|
26
|
+
let durationTicks = null;
|
|
27
|
+
if (outer.id === E.ID.cluster.BLOCK_GROUP) {
|
|
28
|
+
const probe = Math.min(outer.size, BLOCK_GROUP_PROBE_BYTES);
|
|
29
|
+
if (!E.covers(window, outer.body, probe)) {
|
|
30
|
+
window = await supply(outer.body, probe);
|
|
31
|
+
}
|
|
32
|
+
let inner = null;
|
|
33
|
+
let pos = outer.body;
|
|
34
|
+
while (pos < outer.next) {
|
|
35
|
+
const need = headerNeed(pos, outer.next);
|
|
36
|
+
if (!E.covers(window, pos, need)) {
|
|
37
|
+
window = await supply(pos, need);
|
|
38
|
+
}
|
|
39
|
+
const field = E.readElement(window, pos);
|
|
40
|
+
if (!E.fits(field, outer.next))
|
|
41
|
+
return BLOCK_REFUSAL.groupChildren;
|
|
42
|
+
if (field.id === E.ID.blockGroup.BLOCK) {
|
|
43
|
+
inner = field;
|
|
44
|
+
}
|
|
45
|
+
else if (field.id === E.ID.blockGroup.BLOCK_DURATION) {
|
|
46
|
+
const buys = E.uintFits(field.size) && !E.covers(window, field.body, field.size);
|
|
47
|
+
const covering = buys ? await supply(field.body, field.size) : window;
|
|
48
|
+
durationTicks = E.readUint(covering, field);
|
|
49
|
+
}
|
|
50
|
+
pos = field.next;
|
|
51
|
+
}
|
|
52
|
+
if (inner === null)
|
|
53
|
+
return BLOCK_REFUSAL.groupEmpty;
|
|
54
|
+
body = inner.body;
|
|
55
|
+
size = inner.size;
|
|
56
|
+
}
|
|
57
|
+
const need = Math.min(BLOCK_FIELD_BYTES, size);
|
|
58
|
+
if (!E.covers(window, body, need)) {
|
|
59
|
+
window = await supply(body, need);
|
|
60
|
+
}
|
|
61
|
+
const head = readBlockHead(window, body, size);
|
|
62
|
+
if (typeof head === "string")
|
|
63
|
+
return head;
|
|
64
|
+
const numbered = claim(head.track);
|
|
65
|
+
if (numbered === null)
|
|
66
|
+
return null;
|
|
67
|
+
const fault = blockFault(head);
|
|
68
|
+
if (fault !== null)
|
|
69
|
+
return fault;
|
|
70
|
+
const covering = E.covers(window, head.payloadAt, head.payloadSize)
|
|
71
|
+
? window
|
|
72
|
+
: await supply(head.payloadAt, head.payloadSize);
|
|
73
|
+
const payload = E.readBytes(covering, head.payloadAt, head.payloadSize);
|
|
74
|
+
const frameFields = { payload: E.decodeUtf8(payload), clusterTicks, durationTicks, numbered };
|
|
75
|
+
const frame = frameOf(head, frameFields);
|
|
76
|
+
return { track: head.track, frame };
|
|
77
|
+
}
|
|
78
|
+
const TIME_AND_FLAGS_BYTES = 3;
|
|
79
|
+
const BLOCK_FIELD_BYTES = E.MAX_VINT_BYTES + TIME_AND_FLAGS_BYTES;
|
|
80
|
+
const BLOCK_GROUP_PROBE_BYTES = 256;
|
|
81
|
+
const LACING_MASK = 0x06;
|
|
82
|
+
function readBlockHead(window, body, size) {
|
|
83
|
+
const track = E.readVint(window, body, "size");
|
|
84
|
+
if (typeof track === "string")
|
|
85
|
+
return BLOCK_REFUSAL.blockHead;
|
|
86
|
+
const bytesAt = body + track.length;
|
|
87
|
+
if (!E.covers(window, bytesAt, TIME_AND_FLAGS_BYTES))
|
|
88
|
+
return BLOCK_REFUSAL.blockHead;
|
|
89
|
+
const bytes = E.readBytes(window, bytesAt, TIME_AND_FLAGS_BYTES);
|
|
90
|
+
const relTicks = (((bytes[0] << 8) | bytes[1]) << 16) >> 16;
|
|
91
|
+
return {
|
|
92
|
+
track: track.value,
|
|
93
|
+
relTicks,
|
|
94
|
+
laced: (bytes[2] & LACING_MASK) !== 0,
|
|
95
|
+
payloadAt: bytesAt + TIME_AND_FLAGS_BYTES,
|
|
96
|
+
payloadSize: size - (track.length + TIME_AND_FLAGS_BYTES),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function blockFault(block) {
|
|
100
|
+
if (block.laced)
|
|
101
|
+
return BLOCK_REFUSAL.laced;
|
|
102
|
+
if (block.payloadSize < 0)
|
|
103
|
+
return BLOCK_REFUSAL.shortHead;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function frameOf(head, { payload, clusterTicks, durationTicks, numbered }) {
|
|
107
|
+
const lead = numbered ? /^(\d+),/.exec(payload) : null;
|
|
108
|
+
return {
|
|
109
|
+
readOrder: lead === null ? null : Number(lead[1]),
|
|
110
|
+
startTicks: clusterTicks + head.relTicks,
|
|
111
|
+
durationTicks,
|
|
112
|
+
payload,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Element, OpenElement, Window } from "../ebml.js";
|
|
2
|
+
import { type Source } from "../io/source.js";
|
|
3
|
+
export interface Segment {
|
|
4
|
+
/**
|
|
5
|
+
* The absolute offset of the Segment's first child.
|
|
6
|
+
*
|
|
7
|
+
* Every Seek and Cue position is relative to it.
|
|
8
|
+
*/
|
|
9
|
+
body: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SegmentPass {
|
|
13
|
+
source: Source;
|
|
14
|
+
segment: Segment;
|
|
15
|
+
head: Window;
|
|
16
|
+
signal: AbortSignal | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An element and the window its head was read from.
|
|
20
|
+
*
|
|
21
|
+
* For a cluster the window reaches the Timestamp.
|
|
22
|
+
*/
|
|
23
|
+
interface TopLevelHop {
|
|
24
|
+
at: number;
|
|
25
|
+
el: Element | OpenElement;
|
|
26
|
+
window: Window;
|
|
27
|
+
}
|
|
28
|
+
/** Enough for any element's header. */
|
|
29
|
+
export declare const ELEMENT_HEADER_BYTES: number;
|
|
30
|
+
/** Enough to read a cluster's header and its Timestamp. */
|
|
31
|
+
export declare const CLUSTER_PROBE_BYTES = 64;
|
|
32
|
+
/** The cluster children that carry frames. */
|
|
33
|
+
export declare const FRAME_IDS: ReadonlySet<number>;
|
|
34
|
+
export declare const pastSegment: (el: Element, segment: Segment) => boolean;
|
|
35
|
+
export declare const headerNeed: (at: number, end: number) => number;
|
|
36
|
+
/**
|
|
37
|
+
* The one door for reads past the head.
|
|
38
|
+
*
|
|
39
|
+
* The signal is asked here and nowhere else.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readPastHead(pass: SegmentPass, at: number, length: number, held: Window): Promise<Window>;
|
|
42
|
+
export declare function hopTopLevel(pass: SegmentPass,
|
|
43
|
+
/**
|
|
44
|
+
* The end of a sizeless cluster is the walk's discovery.
|
|
45
|
+
*
|
|
46
|
+
* The hop that resumes after it passes it here.
|
|
47
|
+
*/
|
|
48
|
+
from?: number): AsyncGenerator<TopLevelHop>;
|
|
49
|
+
/**
|
|
50
|
+
* A cluster's Timestamp.
|
|
51
|
+
*
|
|
52
|
+
* In a sizeless cluster the search goes as far as the window reaches.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readClusterTime(window: Window, cluster: Element | OpenElement): number | null;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as E from "../ebml.js";
|
|
2
|
+
import { stopIfAborted } from "../io/source.js";
|
|
3
|
+
import { windowOver } from "../io/windows.js";
|
|
4
|
+
export const ELEMENT_HEADER_BYTES = 2 * E.MAX_VINT_BYTES;
|
|
5
|
+
export const CLUSTER_PROBE_BYTES = 64;
|
|
6
|
+
export const FRAME_IDS = new Set([
|
|
7
|
+
E.ID.cluster.SIMPLE_BLOCK,
|
|
8
|
+
E.ID.cluster.BLOCK_GROUP,
|
|
9
|
+
]);
|
|
10
|
+
export const pastSegment = (el, segment) => el.next > segment.end;
|
|
11
|
+
export const headerNeed = (at, end) => Math.min(ELEMENT_HEADER_BYTES, end - at);
|
|
12
|
+
export function readPastHead(pass, at, length, held) {
|
|
13
|
+
stopIfAborted(pass.signal);
|
|
14
|
+
return windowOver(pass.source, at, length, held);
|
|
15
|
+
}
|
|
16
|
+
export async function* hopTopLevel(pass, from = pass.segment.body) {
|
|
17
|
+
let pos = from;
|
|
18
|
+
while (pos < pass.segment.end) {
|
|
19
|
+
const window = await readPastHead(pass, pos, CLUSTER_PROBE_BYTES, pass.head);
|
|
20
|
+
const el = E.readElement(window, pos);
|
|
21
|
+
if (typeof el === "string")
|
|
22
|
+
throw new E.GrammarError();
|
|
23
|
+
const isOpen = "open" in el;
|
|
24
|
+
if (isOpen ? el.id !== E.ID.segment.CLUSTER : pastSegment(el, pass.segment)) {
|
|
25
|
+
throw new E.GrammarError();
|
|
26
|
+
}
|
|
27
|
+
yield { at: pos, el, window };
|
|
28
|
+
if (isOpen)
|
|
29
|
+
return;
|
|
30
|
+
pos = el.next;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function readClusterTime(window, cluster) {
|
|
34
|
+
const end = "open" in cluster ? E.reachOf(window) : cluster.next;
|
|
35
|
+
try {
|
|
36
|
+
for (const field of E.children(window, cluster.body, end - cluster.body)) {
|
|
37
|
+
if (field.id === E.ID.cluster.TIMESTAMP)
|
|
38
|
+
return E.readUint(window, field);
|
|
39
|
+
if (FRAME_IDS.has(field.id))
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type WalkFetch, type Watch } from "../io/source.js";
|
|
2
|
+
import { type Frame } from "./block.js";
|
|
3
|
+
import { type SegmentPass } from "./chain.js";
|
|
4
|
+
export declare function walkFrames(pass: SegmentPass, tracks: ReadonlyMap<number, boolean>, knobs: WalkFetch, { onProgress, signal }: Watch): Promise<Map<number, Frame[]> | null>;
|