video-fps-probe 0.2.0 → 0.4.0-beta.1
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/CHANGELOG.md +94 -0
- package/README.md +33 -15
- package/dist/api/codes.d.ts +6 -0
- package/dist/api/codes.js +88 -0
- package/dist/api/errors.js +1 -1
- package/dist/api/guards.d.ts +2 -0
- package/dist/api/guards.js +2 -1
- package/dist/check/index.js +12 -2
- package/dist/cjs/analyze.d.cts +9 -0
- package/dist/cjs/api/agreement.d.cts +18 -0
- package/dist/cjs/api/codes.d.cts +6 -0
- package/dist/cjs/api/errors.d.cts +12 -0
- package/dist/cjs/api/guards.d.cts +4 -0
- package/dist/cjs/api/summary.d.cts +22 -0
- package/dist/cjs/check/index.d.cts +60 -0
- package/dist/cjs/check.cjs +1 -0
- package/dist/cjs/chunks/build-CT5g6_bs.cjs +1 -0
- package/dist/cjs/chunks/compare-D7RVqlqf.cjs +1 -0
- package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
- package/dist/cjs/chunks/fallback-D9Ak2zFv.cjs +1 -0
- package/dist/cjs/chunks/guards-Dqpjtkdn.cjs +1 -0
- package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
- package/dist/cjs/chunks/isobmff-B_bb5rS5.cjs +1 -0
- package/dist/cjs/chunks/matroska-CLoGzfKA.cjs +1 -0
- package/dist/cjs/chunks/probe-DDLUfr23.cjs +1 -0
- package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
- package/dist/cjs/chunks/reader-DNLR-_tm.cjs +1 -0
- package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
- package/dist/cjs/chunks/runtime-DtgT0xzU.cjs +1 -0
- package/dist/cjs/chunks/segments-i-hPN7H3.cjs +1 -0
- package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
- package/dist/cjs/chunks/stats-D68frkk2.cjs +1 -0
- package/dist/cjs/client/types.d.cts +96 -0
- package/dist/cjs/core/reader.d.cts +47 -0
- package/dist/cjs/core/types.d.cts +149 -0
- package/dist/cjs/engines/types.d.cts +69 -0
- package/dist/cjs/header/index.d.cts +25 -0
- package/dist/cjs/header/limits.d.cts +25 -0
- package/dist/cjs/header.cjs +1 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.d.cts +13 -0
- package/dist/cjs/plugins/types.d.cts +17 -0
- package/dist/cjs/presets/index.d.cts +19 -0
- package/dist/cjs/presets.cjs +1 -0
- package/dist/cjs/probe.d.cts +3 -0
- package/dist/cjs/url/index.d.cts +47 -0
- package/dist/cjs/url.cjs +1 -0
- package/dist/cjs/worker/protocol.d.cts +47 -0
- package/dist/client/compare.js +2 -2
- package/dist/client/create.d.ts +3 -0
- package/dist/client/create.js +8 -0
- package/dist/client/fallback.js +2 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/inline.js +1 -1
- package/dist/client/lazy.js +1 -1
- package/dist/client/probe-client.js +4 -0
- package/dist/client/probe.js +23 -22
- package/dist/client/route.js +20 -0
- package/dist/client/split.js +9 -0
- package/dist/client/stub.js +2 -2
- package/dist/client/types.d.ts +1 -0
- package/dist/core/analyze.js +4 -78
- package/dist/core/dispatch.js +65 -0
- package/dist/core/isobmff/boxes.js +6 -8
- package/dist/core/isobmff/build.js +398 -0
- package/dist/core/isobmff/header.js +654 -0
- package/dist/core/isobmff/index.js +9 -378
- package/dist/core/isobmff/model.js +1 -1
- package/dist/core/isobmff/moof.js +2 -2
- package/dist/core/isobmff/moov.js +14 -9
- package/dist/core/isobmff/track.js +4 -2
- package/dist/core/matroska/ebml.js +1 -1
- package/dist/core/matroska/header.js +9 -4
- package/dist/core/matroska/index.js +7 -3
- package/dist/core/reader.d.ts +3 -12
- package/dist/core/reader.js +21 -32
- package/dist/core/result.js +1 -0
- package/dist/core/stats.js +3 -0
- package/dist/core/types.d.ts +15 -1
- package/dist/engines/common.js +8 -0
- package/dist/engines/mediabunny.js +17 -4
- package/dist/engines/mp4box.js +2 -1
- package/dist/header/index.d.ts +25 -0
- package/dist/header/index.js +146 -0
- package/dist/header/limits.d.ts +25 -0
- package/dist/header/limits.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugins/own.js +5 -2
- package/dist/presets/index.d.ts +1 -0
- package/dist/presets/sources.json +8 -0
- package/dist/probe.js +2 -1
- package/dist/react/store.js +1 -1
- package/dist/standalone/own-isobmff.worker.js +1 -0
- package/dist/standalone/own-matroska.worker.js +1 -0
- package/dist/standalone/own.worker.js +1 -1
- package/dist/url/index.d.ts +47 -0
- package/dist/url/index.js +335 -0
- package/dist/worker/own-isobmff.worker.js +4 -0
- package/dist/worker/own-matroska.worker.js +4 -0
- package/dist/worker/runtime.d.ts +2 -3
- package/dist/worker/runtime.js +2 -97
- package/dist/worker/serve.d.ts +3 -0
- package/dist/worker/serve.js +97 -0
- package/dist/worker/single.js +14 -0
- package/package.json +85 -2
- package/dist/client/inline.d.ts +0 -19
- package/dist/client/pool.d.ts +0 -29
- package/dist/client/probe.d.ts +0 -29
package/dist/core/reader.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FileErrorCode } from './types.js';
|
|
1
2
|
export interface BlobSlice {
|
|
2
3
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
3
4
|
}
|
|
@@ -10,17 +11,9 @@ export interface BlobReaderOptions {
|
|
|
10
11
|
maxWindows?: number;
|
|
11
12
|
signal?: AbortSignal | null;
|
|
12
13
|
}
|
|
13
|
-
export interface ReaderStats {
|
|
14
|
-
reads: number;
|
|
15
|
-
bytesFetched: number;
|
|
16
|
-
bytesTransferred: number;
|
|
17
|
-
}
|
|
18
14
|
export declare const DEFAULT_HEAD_WINDOW: number;
|
|
19
15
|
export declare const DEFAULT_MAX_WINDOWS = 4;
|
|
20
16
|
export declare const MAX_SAFE_OFFSET: number;
|
|
21
|
-
export declare class EndOfFileError extends Error {
|
|
22
|
-
constructor(message: string);
|
|
23
|
-
}
|
|
24
17
|
export declare function toSafeNumber(value: bigint, what?: string): number;
|
|
25
18
|
export declare class BlobReader {
|
|
26
19
|
#private;
|
|
@@ -31,16 +24,12 @@ export declare class BlobReader {
|
|
|
31
24
|
constructor(source: BlobLike, options?: BlobReaderOptions);
|
|
32
25
|
get reads(): number;
|
|
33
26
|
get bytesFetched(): number;
|
|
34
|
-
get bytesTransferred(): number;
|
|
35
|
-
stats(): ReaderStats;
|
|
36
27
|
checkAbort(): void;
|
|
37
28
|
read(offset: number, length: number): Promise<Uint8Array>;
|
|
38
29
|
readMany(ranges: ReadonlyArray<readonly [offset: number, length: number]>, concurrency?: number): Promise<Uint8Array[]>;
|
|
39
30
|
peek(offset: number, length: number): DataView | null;
|
|
40
31
|
view(offset: number, length: number, readAhead?: number): Promise<DataView>;
|
|
41
|
-
viewExact(offset: number, length: number, readAhead?: number): Promise<DataView>;
|
|
42
32
|
bytes(offset: number, length: number, readAhead?: number): Promise<Uint8Array>;
|
|
43
|
-
clearCache(): void;
|
|
44
33
|
}
|
|
45
34
|
export declare function u8(dv: DataView, p: number): number;
|
|
46
35
|
export declare function u16(dv: DataView, p: number): number;
|
|
@@ -54,3 +43,5 @@ export declare function u64Big(dv: DataView, p: number): bigint;
|
|
|
54
43
|
export declare function fixed16_16(dv: DataView, p: number): number;
|
|
55
44
|
export declare function ascii(dv: DataView, p: number, length: number): string;
|
|
56
45
|
export declare function fourcc(dv: DataView, p: number): string;
|
|
46
|
+
export declare function withErrorCode(code: FileErrorCode, message: string): Error;
|
|
47
|
+
export declare function whichErrorCode(e: unknown, fallback?: FileErrorCode): FileErrorCode;
|
package/dist/core/reader.js
CHANGED
|
@@ -2,15 +2,9 @@ import { throwIfAborted } from "./abort.js";
|
|
|
2
2
|
export const DEFAULT_HEAD_WINDOW = 64 * 1024;
|
|
3
3
|
export const DEFAULT_MAX_WINDOWS = 4;
|
|
4
4
|
export const MAX_SAFE_OFFSET = Number.MAX_SAFE_INTEGER;
|
|
5
|
-
|
|
6
|
-
constructor(message) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'EndOfFileError';
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
function assertSafeCount(value, what) {
|
|
5
|
+
function assertSafeCount(value, what, kind = RangeError) {
|
|
12
6
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
13
|
-
throw new
|
|
7
|
+
throw new kind(`${what} must be a non-negative safe integer, got ${value}`);
|
|
14
8
|
}
|
|
15
9
|
export function toSafeNumber(value, what = 'value') {
|
|
16
10
|
if (value > BigInt(MAX_SAFE_OFFSET) || value < -BigInt(MAX_SAFE_OFFSET))
|
|
@@ -27,15 +21,14 @@ export class BlobReader {
|
|
|
27
21
|
#covered = [];
|
|
28
22
|
#reads = 0;
|
|
29
23
|
#unique = 0;
|
|
30
|
-
#transferred = 0;
|
|
31
24
|
constructor(source, options = {}) {
|
|
32
|
-
assertSafeCount(source.size, 'blob size');
|
|
25
|
+
assertSafeCount(source.size, 'blob size', TypeError);
|
|
33
26
|
this.#source = source;
|
|
34
27
|
this.size = source.size;
|
|
35
28
|
this.headWindow = options.headWindow ?? DEFAULT_HEAD_WINDOW;
|
|
36
29
|
this.maxWindows = Math.max(1, options.maxWindows ?? DEFAULT_MAX_WINDOWS);
|
|
37
30
|
this.signal = options.signal ?? null;
|
|
38
|
-
assertSafeCount(this.headWindow, 'headWindow');
|
|
31
|
+
assertSafeCount(this.headWindow, 'headWindow', TypeError);
|
|
39
32
|
}
|
|
40
33
|
get reads() {
|
|
41
34
|
return this.#reads;
|
|
@@ -43,12 +36,6 @@ export class BlobReader {
|
|
|
43
36
|
get bytesFetched() {
|
|
44
37
|
return this.#unique;
|
|
45
38
|
}
|
|
46
|
-
get bytesTransferred() {
|
|
47
|
-
return this.#transferred;
|
|
48
|
-
}
|
|
49
|
-
stats() {
|
|
50
|
-
return { reads: this.#reads, bytesFetched: this.#unique, bytesTransferred: this.#transferred };
|
|
51
|
-
}
|
|
52
39
|
checkAbort() {
|
|
53
40
|
throwIfAborted(this.signal);
|
|
54
41
|
}
|
|
@@ -61,12 +48,18 @@ export class BlobReader {
|
|
|
61
48
|
if (end === offset)
|
|
62
49
|
return new Uint8Array(0);
|
|
63
50
|
this.checkAbort();
|
|
64
|
-
|
|
51
|
+
let ab;
|
|
52
|
+
try {
|
|
53
|
+
ab = await this.#source.slice(offset, end).arrayBuffer();
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
this.checkAbort();
|
|
57
|
+
throw e?.name === 'AbortError' ? e : withErrorCode('read-failed', `file read failed: ${e}`);
|
|
58
|
+
}
|
|
65
59
|
this.checkAbort();
|
|
66
60
|
this.#reads++;
|
|
67
|
-
this.#transferred += ab.byteLength;
|
|
68
61
|
if (ab.byteLength !== end - offset) {
|
|
69
|
-
throw
|
|
62
|
+
throw withErrorCode('read-failed', `file read failed: short read at ${offset}: expected ${end - offset} bytes, got ${ab.byteLength}`);
|
|
70
63
|
}
|
|
71
64
|
this.#cover(offset, end);
|
|
72
65
|
return new Uint8Array(ab);
|
|
@@ -105,7 +98,7 @@ export class BlobReader {
|
|
|
105
98
|
async view(offset, length, readAhead = this.headWindow) {
|
|
106
99
|
assertSafeCount(offset, 'offset');
|
|
107
100
|
assertSafeCount(length, 'length');
|
|
108
|
-
assertSafeCount(readAhead, 'readAhead');
|
|
101
|
+
assertSafeCount(readAhead, 'readAhead', TypeError);
|
|
109
102
|
if (offset > this.size)
|
|
110
103
|
throw new RangeError(`offset ${offset} is beyond the end of the file (${this.size})`);
|
|
111
104
|
const end = Math.min(this.size, offset + length);
|
|
@@ -132,20 +125,10 @@ export class BlobReader {
|
|
|
132
125
|
this.#windows.length = this.maxWindows;
|
|
133
126
|
return new DataView(buf.buffer, buf.byteOffset, end - offset);
|
|
134
127
|
}
|
|
135
|
-
async viewExact(offset, length, readAhead = this.headWindow) {
|
|
136
|
-
const dv = await this.view(offset, length, readAhead);
|
|
137
|
-
if (dv.byteLength < length) {
|
|
138
|
-
throw new EndOfFileError(`unexpected end of file: wanted ${length} bytes at ${offset}, file size ${this.size}`);
|
|
139
|
-
}
|
|
140
|
-
return dv;
|
|
141
|
-
}
|
|
142
128
|
async bytes(offset, length, readAhead = this.headWindow) {
|
|
143
129
|
const dv = await this.view(offset, length, readAhead);
|
|
144
130
|
return new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
|
145
131
|
}
|
|
146
|
-
clearCache() {
|
|
147
|
-
this.#windows = [];
|
|
148
|
-
}
|
|
149
132
|
#cover(start, end) {
|
|
150
133
|
const list = this.#covered;
|
|
151
134
|
let lo = 0;
|
|
@@ -213,5 +196,11 @@ export function ascii(dv, p, length) {
|
|
|
213
196
|
return s;
|
|
214
197
|
}
|
|
215
198
|
export function fourcc(dv, p) {
|
|
216
|
-
return ascii(dv, p, 4);
|
|
199
|
+
return ascii(dv, p, 4).replace(/[^ -~]/g, '?');
|
|
200
|
+
}
|
|
201
|
+
export function withErrorCode(code, message) {
|
|
202
|
+
return Object.assign(Error(message), { errorCode: code });
|
|
203
|
+
}
|
|
204
|
+
export function whichErrorCode(e, fallback = 'internal') {
|
|
205
|
+
return e?.errorCode ?? (e instanceof RangeError ? 'corrupt' : fallback);
|
|
217
206
|
}
|
package/dist/core/result.js
CHANGED
package/dist/core/stats.js
CHANGED
|
@@ -134,6 +134,7 @@ function emptyStats(timescale, warnings) {
|
|
|
134
134
|
minDelta: null,
|
|
135
135
|
maxDelta: null,
|
|
136
136
|
medianDelta: null,
|
|
137
|
+
medianFrameInterval: null,
|
|
137
138
|
meanDelta: null,
|
|
138
139
|
tolerance: null,
|
|
139
140
|
minFps: null,
|
|
@@ -235,6 +236,8 @@ function applyHistogram(base, hist, tickQuantized) {
|
|
|
235
236
|
base.minDelta = minD;
|
|
236
237
|
base.maxDelta = maxD;
|
|
237
238
|
base.medianDelta = median;
|
|
239
|
+
const interval = rateFromTicks(ts, median);
|
|
240
|
+
base.medianFrameInterval = interval && makeRational(interval.den, interval.num);
|
|
238
241
|
base.meanDelta = sum / total;
|
|
239
242
|
base.tolerance = tol;
|
|
240
243
|
base.minFps = rateFromTicks(ts, maxD);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type ContainerKind = 'mp4' | 'mov' | 'webm' | 'matroska' | 'unknown';
|
|
|
17
17
|
export type EngineId = 'own' | 'mediabunny' | 'mp4box' | 'rvfc';
|
|
18
18
|
export type MatroskaMode = 'header' | 'probe' | 'exact' | 'auto';
|
|
19
19
|
export type ScanDepth = 'header' | 'probe' | 'exact';
|
|
20
|
-
export type TrackField = 'codec' | 'dimensions' | 'rotation' | 'frames' | 'presentedFrames' | 'durationSec' | 'startSec' | 'avgFps' | 'nominalFps' | 'dominantFps' | 'minFps' | 'maxFps' | 'isVfr' | 'duplicateTimestamps';
|
|
20
|
+
export type TrackField = 'codec' | 'dimensions' | 'rotation' | 'frames' | 'presentedFrames' | 'durationSec' | 'startSec' | 'avgFps' | 'nominalFps' | 'dominantFps' | 'minFps' | 'maxFps' | 'isVfr' | 'duplicateTimestamps' | 'medianFrameInterval';
|
|
21
21
|
export type RateSegmentKind = 'steady' | 'variable';
|
|
22
22
|
export interface RateSegment {
|
|
23
23
|
startSec: number;
|
|
@@ -74,6 +74,8 @@ export interface VideoTrackResult {
|
|
|
74
74
|
timestampsQuantized: boolean;
|
|
75
75
|
isStill: boolean;
|
|
76
76
|
rateProfile?: RateProfile | null;
|
|
77
|
+
medianFrameInterval?: Rational | null;
|
|
78
|
+
enabled?: boolean;
|
|
77
79
|
provenance: Partial<Record<TrackField, Provenance>>;
|
|
78
80
|
warnings: string[];
|
|
79
81
|
}
|
|
@@ -82,6 +84,16 @@ export interface IoStats {
|
|
|
82
84
|
bytesRead: number;
|
|
83
85
|
ms: number;
|
|
84
86
|
}
|
|
87
|
+
export type FileErrorCode = 'empty-file' | 'unrecognised' | 'unsupported-container' | 'unsupported' | 'too-large' | 'no-header' | 'truncated' | 'corrupt' | 'no-duration' | 'byte-limit' | 'read-limit' | 'time-limit' | 'structure-limit' | 'read-failed' | 'load-failed' | 'engine-failed' | 'internal';
|
|
88
|
+
export type LayoutKind = 'faststart' | 'moov-at-end' | 'fragmented';
|
|
89
|
+
export interface ContainerLayout {
|
|
90
|
+
kind: LayoutKind;
|
|
91
|
+
moovOffset: number | null;
|
|
92
|
+
moovSize: number | null;
|
|
93
|
+
sidx: boolean;
|
|
94
|
+
emptyMoov: boolean;
|
|
95
|
+
}
|
|
96
|
+
export type WarningCode = 'invalid-timescale' | 'truncated-mdat' | 'truncated-box' | 'trailing-data' | 'fragment-index-unread' | 'duration-unresolved' | 'fragments-ignored' | 'structure-limit';
|
|
85
97
|
export interface FileResult {
|
|
86
98
|
name: string;
|
|
87
99
|
size: number;
|
|
@@ -93,6 +105,8 @@ export interface FileResult {
|
|
|
93
105
|
audioTrackCount: number;
|
|
94
106
|
primaryTrackId: number | null;
|
|
95
107
|
error: string | null;
|
|
108
|
+
errorCode?: FileErrorCode | null;
|
|
109
|
+
layout?: ContainerLayout | null;
|
|
96
110
|
warnings: string[];
|
|
97
111
|
io: IoStats;
|
|
98
112
|
engine: EngineId;
|
package/dist/engines/common.js
CHANGED
|
@@ -53,6 +53,12 @@ export function isAbort(e, signal) {
|
|
|
53
53
|
export function newResult(name, size, engine) {
|
|
54
54
|
return { name, size, ...emptyAnalysis('unknown'), io: { reads: 0, bytesRead: 0, ms: 0 }, engine };
|
|
55
55
|
}
|
|
56
|
+
const printable = (warnings) => warnings.map((w) => w.replace(/[^\x20-\x7e\xa0-\uffff]/g, '?'));
|
|
57
|
+
export function printableWarnings(result) {
|
|
58
|
+
result.warnings = printable(result.warnings);
|
|
59
|
+
for (const t of result.videoTracks)
|
|
60
|
+
t.warnings = printable(t.warnings);
|
|
61
|
+
}
|
|
56
62
|
export function ffprobeRotation(matrix) {
|
|
57
63
|
const a = matrix[0] / 65536;
|
|
58
64
|
const b = matrix[1] / 65536;
|
|
@@ -102,6 +108,7 @@ export function applyStats(track, stats, source = 'exact') {
|
|
|
102
108
|
track.dominantFps = stats.dominantFps;
|
|
103
109
|
track.minFps = stats.minFps;
|
|
104
110
|
track.maxFps = stats.maxFps;
|
|
111
|
+
track.medianFrameInterval = stats.medianFrameInterval;
|
|
105
112
|
track.isVfr = stats.isVfr;
|
|
106
113
|
track.vfrRatio = stats.vfrRatio;
|
|
107
114
|
track.distinctDeltas = stats.distinctDeltas;
|
|
@@ -120,5 +127,6 @@ export function applyStats(track, stats, source = 'exact') {
|
|
|
120
127
|
maxFps: source,
|
|
121
128
|
isVfr: source,
|
|
122
129
|
duplicateTimestamps: source,
|
|
130
|
+
medianFrameInterval: source,
|
|
123
131
|
});
|
|
124
132
|
}
|
|
@@ -3,7 +3,7 @@ import { throwIfAborted } from "../core/abort.js";
|
|
|
3
3
|
import { resolveOptions } from "../core/options.js";
|
|
4
4
|
import { emptyTrack } from "../core/result.js";
|
|
5
5
|
import { analyzeTimeline } from "../core/stats.js";
|
|
6
|
-
import { applyStats, approxRational, codecNameFromFourcc, errorMessage, isAbort, newResult } from "./common.js";
|
|
6
|
+
import { applyStats, approxRational, codecNameFromFourcc, errorMessage, isAbort, newResult, printableWarnings } from "./common.js";
|
|
7
7
|
import { CountingBlob, elapsed, now } from "./counting.js";
|
|
8
8
|
export const MEDIABUNNY_VERSION = '1.62.1';
|
|
9
9
|
const CODEC_NAMES = {
|
|
@@ -145,7 +145,18 @@ async function probe(counted, result, opts, t0) {
|
|
|
145
145
|
];
|
|
146
146
|
const source = new CustomSource({
|
|
147
147
|
getSize: () => counted.size,
|
|
148
|
-
read: async (start, end) =>
|
|
148
|
+
read: async (start, end) => {
|
|
149
|
+
let ab;
|
|
150
|
+
try {
|
|
151
|
+
ab = await counted.slice(start, end).arrayBuffer();
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw isAbort(e, signal) ? e : Error(`file read failed: ${e}`);
|
|
155
|
+
}
|
|
156
|
+
if (ab.byteLength !== end - start)
|
|
157
|
+
throw Error(`file read failed: short read at ${start}: expected ${end - start} bytes, got ${ab.byteLength}`);
|
|
158
|
+
return new Uint8Array(ab);
|
|
159
|
+
},
|
|
149
160
|
prefetchProfile: 'fileSystem',
|
|
150
161
|
handleUnhandledError: (e) => {
|
|
151
162
|
unhandled.push(errorMessage(e));
|
|
@@ -160,7 +171,8 @@ async function probe(counted, result, opts, t0) {
|
|
|
160
171
|
catch (e) {
|
|
161
172
|
if (isAbort(e, signal))
|
|
162
173
|
throw e;
|
|
163
|
-
|
|
174
|
+
const message = errorMessage(e);
|
|
175
|
+
result.error = message.startsWith('file read failed') ? message : `not a recognised container: ${message}`;
|
|
164
176
|
return;
|
|
165
177
|
}
|
|
166
178
|
result.container = containerOf(format);
|
|
@@ -176,7 +188,7 @@ async function probe(counted, result, opts, t0) {
|
|
|
176
188
|
works.push(await collectTrack(track, quantized, signal, maxPackets));
|
|
177
189
|
}
|
|
178
190
|
catch (e) {
|
|
179
|
-
if (isAbort(e, signal))
|
|
191
|
+
if (isAbort(e, signal) || errorMessage(e).startsWith('file read failed'))
|
|
180
192
|
throw e;
|
|
181
193
|
result.warnings.push(`track ${track.id}: ${errorMessage(e)}`);
|
|
182
194
|
}
|
|
@@ -235,6 +247,7 @@ export async function analyzeWithMediabunny(blob, name, opts = {}) {
|
|
|
235
247
|
}
|
|
236
248
|
if (result.mediabunny === undefined)
|
|
237
249
|
result.io = counted.io(t0);
|
|
250
|
+
printableWarnings(result);
|
|
238
251
|
return result;
|
|
239
252
|
}
|
|
240
253
|
export const containers = 'any';
|
package/dist/engines/mp4box.js
CHANGED
|
@@ -5,7 +5,7 @@ import { BlobReader, fourcc, u32, u64 } from "../core/reader.js";
|
|
|
5
5
|
import { emptyTrack } from "../core/result.js";
|
|
6
6
|
import { sniff } from "../core/sniff.js";
|
|
7
7
|
import { analyzeTimeline } from "../core/stats.js";
|
|
8
|
-
import { applyStats, codecNameFromFourcc, errorMessage, ffprobeRotation, isAbort, newResult } from "./common.js";
|
|
8
|
+
import { applyStats, codecNameFromFourcc, errorMessage, ffprobeRotation, isAbort, newResult, printableWarnings } from "./common.js";
|
|
9
9
|
import { CountingBlob, now } from "./counting.js";
|
|
10
10
|
export const MP4BOX_VERSION = '2.4.1';
|
|
11
11
|
const MAX_TOP_LEVEL_BOXES = 5_000_000;
|
|
@@ -423,6 +423,7 @@ export async function analyzeWithMp4box(blob, name, opts = {}) {
|
|
|
423
423
|
result.error = errorMessage(e);
|
|
424
424
|
}
|
|
425
425
|
result.io = counted.io(t0);
|
|
426
|
+
printableWarnings(result);
|
|
426
427
|
return result;
|
|
427
428
|
}
|
|
428
429
|
export const containers = ['isobmff'];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type BlobLike } from '../core/reader.js';
|
|
2
|
+
import type { FileResult, IoStats } from '../core/types.js';
|
|
3
|
+
import { type HeaderLimits } from './limits.js';
|
|
4
|
+
export { errorClass, errorCodeOf, warningCodeOf, warningCodesOf, type ErrorClass } from '../api/codes.js';
|
|
5
|
+
export { isAbortError, isTimeoutError } from '../api/guards.js';
|
|
6
|
+
export { HEADER_DEFAULT_LIMITS, type HeaderLimits } from './limits.js';
|
|
7
|
+
export type { BlobLike, BlobSlice } from '../core/reader.js';
|
|
8
|
+
export type * from '../core/types.js';
|
|
9
|
+
export type FragmentedDuration = 'header' | 'index';
|
|
10
|
+
export interface HeaderOptions {
|
|
11
|
+
name?: string;
|
|
12
|
+
signal?: AbortSignal | null;
|
|
13
|
+
limits?: HeaderLimits;
|
|
14
|
+
readAhead?: {
|
|
15
|
+
head?: number;
|
|
16
|
+
tail?: number;
|
|
17
|
+
};
|
|
18
|
+
fragmentedDuration?: FragmentedDuration;
|
|
19
|
+
}
|
|
20
|
+
export type HeaderResult = FileResult & {
|
|
21
|
+
io: IoStats & {
|
|
22
|
+
bytesRequested: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare function analyzeHeader(source: Blob | BlobLike, options?: HeaderOptions): Promise<HeaderResult>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { throwIfAborted } from "../core/abort.js";
|
|
2
|
+
import { analyzeIsoBmffHeader } from "../core/isobmff/header.js";
|
|
3
|
+
import { DEFAULT_READ_AHEAD } from "../core/options.js";
|
|
4
|
+
import { BlobReader, whichErrorCode } from "../core/reader.js";
|
|
5
|
+
import { emptyAnalysis } from "../core/result.js";
|
|
6
|
+
import { sniff } from "../core/sniff.js";
|
|
7
|
+
import { HEADER_DEFAULT_LIMITS, LimitedSource } from "./limits.js";
|
|
8
|
+
export { errorClass, errorCodeOf, warningCodeOf, warningCodesOf } from "../api/codes.js";
|
|
9
|
+
export { isAbortError, isTimeoutError } from "../api/guards.js";
|
|
10
|
+
export { HEADER_DEFAULT_LIMITS } from "./limits.js";
|
|
11
|
+
const SNIFF_BYTES = 1024;
|
|
12
|
+
const MIN_HEAD = SNIFF_BYTES;
|
|
13
|
+
const MIN_TAIL = 16;
|
|
14
|
+
function shown(value) {
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
if (typeof value === 'object' && value !== null)
|
|
18
|
+
return Array.isArray(value) ? 'an array' : 'an object';
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
function count(value, what, min = 0, infinite = false) {
|
|
22
|
+
if (infinite && value === Number.POSITIVE_INFINITY)
|
|
23
|
+
return value;
|
|
24
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min) {
|
|
25
|
+
throw new TypeError(`${what} must be a non-negative safe integer${min > 0 ? ` of at least ${min}` : ''}${infinite ? ' or Infinity' : ''}, got ${shown(value)}`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function record(value, what) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return {};
|
|
32
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
33
|
+
throw new TypeError(`${what} must be an object, got ${shown(value)}`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function resolveHeaderOptions(source, options) {
|
|
37
|
+
const src = source;
|
|
38
|
+
if (typeof src !== 'object' || src === null || typeof src.slice !== 'function')
|
|
39
|
+
throw new TypeError(`source must be a Blob or a BlobLike with a size and a slice method, got ${shown(source)}`);
|
|
40
|
+
const size = count(src.size, 'source.size');
|
|
41
|
+
const o = record(options, 'options');
|
|
42
|
+
if (o.name !== undefined && typeof o.name !== 'string')
|
|
43
|
+
throw new TypeError(`name must be a string, got ${shown(o.name)}`);
|
|
44
|
+
const signal = o.signal ?? null;
|
|
45
|
+
if (signal !== null) {
|
|
46
|
+
const s = signal;
|
|
47
|
+
if (typeof s !== 'object' || typeof s.aborted !== 'boolean' || typeof s.addEventListener !== 'function' || typeof s.removeEventListener !== 'function')
|
|
48
|
+
throw new TypeError(`signal must be an AbortSignal, got ${shown(signal)}`);
|
|
49
|
+
}
|
|
50
|
+
const l = record(o.limits, 'limits');
|
|
51
|
+
const limits = { ...HEADER_DEFAULT_LIMITS };
|
|
52
|
+
for (const key of Object.keys(HEADER_DEFAULT_LIMITS))
|
|
53
|
+
if (l[key] !== undefined)
|
|
54
|
+
limits[key] = count(l[key], `limits.${key}`, 0, true);
|
|
55
|
+
const r = record(o.readAhead, 'readAhead');
|
|
56
|
+
for (const key of Object.keys(r))
|
|
57
|
+
if (key !== 'head' && key !== 'tail' && r[key] !== undefined)
|
|
58
|
+
throw new TypeError(`readAhead takes only head and tail, got ${key}`);
|
|
59
|
+
const head = r.head === undefined ? DEFAULT_READ_AHEAD.head : count(r.head, 'readAhead.head', MIN_HEAD);
|
|
60
|
+
const tail = r.tail === undefined ? DEFAULT_READ_AHEAD.tail : count(r.tail, 'readAhead.tail', MIN_TAIL);
|
|
61
|
+
const mode = o.fragmentedDuration ?? 'index';
|
|
62
|
+
if (mode !== 'index' && mode !== 'header')
|
|
63
|
+
throw new TypeError(`fragmentedDuration must be 'header' or 'index', got ${shown(mode)}`);
|
|
64
|
+
const name = o.name ?? (typeof File !== 'undefined' && source instanceof File ? source.name : '');
|
|
65
|
+
return { name, size, signal: signal, limits, head, tail, index: mode === 'index' };
|
|
66
|
+
}
|
|
67
|
+
function docTypeOf(head) {
|
|
68
|
+
let p = 4;
|
|
69
|
+
const vint = (marker) => {
|
|
70
|
+
if (p >= head.length)
|
|
71
|
+
return null;
|
|
72
|
+
const first = head[p];
|
|
73
|
+
let len = 1;
|
|
74
|
+
while (len <= 8 && !(first & (0x80 >> (len - 1))))
|
|
75
|
+
len++;
|
|
76
|
+
if (len > 8 || p + len > head.length)
|
|
77
|
+
return null;
|
|
78
|
+
let v = marker ? first : first & (0xff >> len);
|
|
79
|
+
for (let i = 1; i < len; i++)
|
|
80
|
+
v = v * 256 + head[p + i];
|
|
81
|
+
p += len;
|
|
82
|
+
return v;
|
|
83
|
+
};
|
|
84
|
+
const size = vint(false);
|
|
85
|
+
if (size === null)
|
|
86
|
+
return null;
|
|
87
|
+
const end = Math.min(head.length, p + size);
|
|
88
|
+
while (p < end) {
|
|
89
|
+
const id = vint(true);
|
|
90
|
+
const n = vint(false);
|
|
91
|
+
if (id === null || n === null || p + n > end)
|
|
92
|
+
return null;
|
|
93
|
+
if (id === 0x4282)
|
|
94
|
+
return String.fromCharCode(...head.subarray(p, p + n)).replace(/\0+$/, '');
|
|
95
|
+
p += n;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function failed(analysis, message, code) {
|
|
100
|
+
analysis.error = message;
|
|
101
|
+
analysis.errorCode = code;
|
|
102
|
+
return analysis;
|
|
103
|
+
}
|
|
104
|
+
async function analyzeSource(reader, limited, o) {
|
|
105
|
+
const analysis = { ...emptyAnalysis('unknown'), scan: 'header', errorCode: null, layout: null };
|
|
106
|
+
const size = reader.size;
|
|
107
|
+
if (size === 0)
|
|
108
|
+
return failed(analysis, 'not a recognised container: empty file', 'empty-file');
|
|
109
|
+
const head = await reader.bytes(0, Math.min(SNIFF_BYTES, size), Math.min(size, o.head, limited.bytesLeft));
|
|
110
|
+
const sniffed = sniff(head);
|
|
111
|
+
if (sniffed.kind === 'unknown')
|
|
112
|
+
return failed(analysis, `not a recognised container: ${sniffed.reason}`, 'unrecognised');
|
|
113
|
+
if (sniffed.kind === 'matroska') {
|
|
114
|
+
analysis.container = docTypeOf(head) === 'webm' ? 'webm' : 'matroska';
|
|
115
|
+
return failed(analysis, `container not read by the header entry: ${analysis.container}`, 'unsupported-container');
|
|
116
|
+
}
|
|
117
|
+
const { maxTracks, maxTopLevelBoxes } = o.limits;
|
|
118
|
+
return analyzeIsoBmffHeader(reader, limited, { head: o.head, tail: o.tail, maxTracks, maxTopLevelBoxes, index: o.index });
|
|
119
|
+
}
|
|
120
|
+
export async function analyzeHeader(source, options = {}) {
|
|
121
|
+
const t0 = performance.now();
|
|
122
|
+
const o = resolveHeaderOptions(source, options);
|
|
123
|
+
throwIfAborted(o.signal);
|
|
124
|
+
const limited = new LimitedSource(source, o.size, o.limits, o.signal);
|
|
125
|
+
const reader = new BlobReader(limited, { headWindow: o.head, signal: o.signal });
|
|
126
|
+
let analysis;
|
|
127
|
+
try {
|
|
128
|
+
analysis = await analyzeSource(reader, limited, o);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
throwIfAborted(o.signal);
|
|
132
|
+
const limit = limited.take();
|
|
133
|
+
analysis = { ...emptyAnalysis('unknown'), scan: 'header', errorCode: null, layout: null };
|
|
134
|
+
failed(analysis, limit ? limit.message : e instanceof Error ? e.message || e.name : String(e), whichErrorCode(limit ?? e));
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
limited.dispose();
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
name: o.name,
|
|
141
|
+
size: o.size,
|
|
142
|
+
...analysis,
|
|
143
|
+
io: { reads: reader.reads, bytesRead: reader.bytesFetched, ms: performance.now() - t0, bytesRequested: limited.bytesRequested },
|
|
144
|
+
engine: 'own',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BlobLike, BlobSlice } from '../core/reader.js';
|
|
2
|
+
export interface HeaderLimits {
|
|
3
|
+
maxBytes?: number;
|
|
4
|
+
maxReads?: number;
|
|
5
|
+
maxMs?: number;
|
|
6
|
+
maxTracks?: number;
|
|
7
|
+
maxTopLevelBoxes?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const HEADER_DEFAULT_LIMITS: Readonly<Required<HeaderLimits>>;
|
|
10
|
+
export type LimitCode = 'byte-limit' | 'read-limit' | 'time-limit' | 'structure-limit';
|
|
11
|
+
export declare class LimitError extends Error {
|
|
12
|
+
readonly errorCode: LimitCode;
|
|
13
|
+
constructor(code: LimitCode, message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class LimitedSource implements BlobLike {
|
|
16
|
+
#private;
|
|
17
|
+
readonly size: number;
|
|
18
|
+
constructor(inner: BlobLike, size: number, limits: Required<Pick<HeaderLimits, 'maxBytes' | 'maxReads' | 'maxMs'>>, caller: AbortSignal | null);
|
|
19
|
+
get bytesLeft(): number;
|
|
20
|
+
get bytesRequested(): number;
|
|
21
|
+
checkTime(): void;
|
|
22
|
+
take(): LimitError | null;
|
|
23
|
+
slice(start?: number, end?: number): BlobSlice;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export const HEADER_DEFAULT_LIMITS = Object.freeze({
|
|
2
|
+
maxBytes: 16 * 1024 * 1024,
|
|
3
|
+
maxReads: 8,
|
|
4
|
+
maxMs: 10_000,
|
|
5
|
+
maxTracks: 64,
|
|
6
|
+
maxTopLevelBoxes: 1024,
|
|
7
|
+
});
|
|
8
|
+
export class LimitError extends Error {
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'LimitError';
|
|
12
|
+
this.errorCode = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const noop = () => { };
|
|
16
|
+
const MAX_TIMER_MS = 0x7fffffff;
|
|
17
|
+
function sourceError(message) {
|
|
18
|
+
return Object.assign(new Error(message), { name: '' });
|
|
19
|
+
}
|
|
20
|
+
function bufferOf(value) {
|
|
21
|
+
if (ArrayBuffer.isView(value)) {
|
|
22
|
+
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
23
|
+
return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength && bytes.buffer instanceof ArrayBuffer ? bytes.buffer : bytes.slice().buffer;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value?.byteLength !== 'number')
|
|
26
|
+
throw sourceError(`the source returned ${value === null ? 'null' : typeof value} instead of bytes`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
export class LimitedSource {
|
|
30
|
+
size;
|
|
31
|
+
#inner;
|
|
32
|
+
#maxBytes;
|
|
33
|
+
#maxReads;
|
|
34
|
+
#maxMs;
|
|
35
|
+
#deadline;
|
|
36
|
+
#controller = new AbortController();
|
|
37
|
+
#stopped;
|
|
38
|
+
#caller;
|
|
39
|
+
#forward = () => this.#controller.abort(this.#caller?.reason);
|
|
40
|
+
#timer = null;
|
|
41
|
+
#timedOut = false;
|
|
42
|
+
#reads = 0;
|
|
43
|
+
#bytes = 0;
|
|
44
|
+
#tripped = null;
|
|
45
|
+
constructor(inner, size, limits, caller) {
|
|
46
|
+
this.size = size;
|
|
47
|
+
this.#inner = inner;
|
|
48
|
+
this.#maxBytes = limits.maxBytes;
|
|
49
|
+
this.#maxReads = limits.maxReads;
|
|
50
|
+
this.#maxMs = limits.maxMs;
|
|
51
|
+
this.#deadline = performance.now() + limits.maxMs;
|
|
52
|
+
this.#caller = caller;
|
|
53
|
+
const internal = this.#controller.signal;
|
|
54
|
+
this.#stopped = new Promise((_, reject) => internal.addEventListener('abort', () => reject(internal.reason), { once: true }));
|
|
55
|
+
this.#stopped.catch(noop);
|
|
56
|
+
if (caller) {
|
|
57
|
+
if (caller.aborted)
|
|
58
|
+
this.#forward();
|
|
59
|
+
else
|
|
60
|
+
caller.addEventListener('abort', this.#forward, { once: true });
|
|
61
|
+
}
|
|
62
|
+
if (Number.isFinite(limits.maxMs) && !internal.aborted)
|
|
63
|
+
this.#arm(limits.maxMs);
|
|
64
|
+
}
|
|
65
|
+
#arm(left) {
|
|
66
|
+
const step = Math.min(left, MAX_TIMER_MS);
|
|
67
|
+
this.#timer = setTimeout(() => {
|
|
68
|
+
if (left > step)
|
|
69
|
+
return this.#arm(left - step);
|
|
70
|
+
this.#timer = null;
|
|
71
|
+
this.#timedOut = true;
|
|
72
|
+
this.#controller.abort(new LimitError('time-limit', `limit reached: maxMs ${this.#maxMs}`));
|
|
73
|
+
}, step);
|
|
74
|
+
}
|
|
75
|
+
get bytesLeft() {
|
|
76
|
+
return this.#maxBytes - this.#bytes;
|
|
77
|
+
}
|
|
78
|
+
get bytesRequested() {
|
|
79
|
+
return this.#bytes;
|
|
80
|
+
}
|
|
81
|
+
checkTime() {
|
|
82
|
+
if (this.#timedOut || performance.now() >= this.#deadline)
|
|
83
|
+
throw this.#trip('time-limit', `limit reached: maxMs ${this.#maxMs}`);
|
|
84
|
+
}
|
|
85
|
+
take() {
|
|
86
|
+
const t = this.#tripped;
|
|
87
|
+
this.#tripped = null;
|
|
88
|
+
return t;
|
|
89
|
+
}
|
|
90
|
+
slice(start = 0, end = this.size) {
|
|
91
|
+
return { arrayBuffer: () => this.#fetch(start, end) };
|
|
92
|
+
}
|
|
93
|
+
dispose() {
|
|
94
|
+
if (this.#timer !== null)
|
|
95
|
+
clearTimeout(this.#timer);
|
|
96
|
+
this.#timer = null;
|
|
97
|
+
this.#caller?.removeEventListener('abort', this.#forward);
|
|
98
|
+
}
|
|
99
|
+
#trip(code, message) {
|
|
100
|
+
this.#tripped = new LimitError(code, message);
|
|
101
|
+
return this.#tripped;
|
|
102
|
+
}
|
|
103
|
+
#admission(bytes, offset) {
|
|
104
|
+
if (this.#reads + 1 > this.#maxReads)
|
|
105
|
+
return this.#trip('read-limit', `limit reached: maxReads ${this.#maxReads}`);
|
|
106
|
+
if (this.#bytes + bytes > this.#maxBytes)
|
|
107
|
+
return this.#trip('byte-limit', `limit reached: maxBytes ${this.#maxBytes} (${this.#bytes + bytes - this.#maxBytes} more bytes needed at offset ${offset})`);
|
|
108
|
+
try {
|
|
109
|
+
this.checkTime();
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return e;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
#fetch(start, end) {
|
|
117
|
+
const length = Math.max(0, end - start);
|
|
118
|
+
const trip = this.#admission(length, start);
|
|
119
|
+
if (trip)
|
|
120
|
+
return Promise.reject(trip);
|
|
121
|
+
this.#reads++;
|
|
122
|
+
this.#bytes += length;
|
|
123
|
+
let pending;
|
|
124
|
+
try {
|
|
125
|
+
const inner = this.#inner;
|
|
126
|
+
pending = Promise.resolve(typeof inner.read === 'function' ? inner.read(start, end, this.#controller.signal) : inner.slice(start, end).arrayBuffer()).then(bufferOf);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
pending = Promise.reject(e);
|
|
130
|
+
}
|
|
131
|
+
pending.catch(noop);
|
|
132
|
+
return Promise.race([pending, this.#stopped]).catch((e) => {
|
|
133
|
+
if (e instanceof LimitError)
|
|
134
|
+
this.#tripped = e;
|
|
135
|
+
else if (!this.#controller.signal.aborted && e?.name === 'AbortError')
|
|
136
|
+
throw sourceError(String(e));
|
|
137
|
+
throw e;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { analyze, type InThreadOptions } from './analyze.js';
|
|
2
2
|
export { compareResults, type Agreement, type ComparedField, type ComparedFieldName, type ComparedValue } from './api/agreement.js';
|
|
3
|
+
export { errorClass, errorCodeOf, warningCodeOf, warningCodesOf, type ErrorClass } from './api/codes.js';
|
|
3
4
|
export { ProbeError, type ProbeErrorCode } from './api/errors.js';
|
|
4
5
|
export { isAbortError, isTimeoutError } from './api/guards.js';
|
|
5
6
|
export { summarize, type VideoFpsSummary } from './api/summary.js';
|