video-fps-probe 0.3.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +18 -12
  3. package/dist/api/codes.d.ts +6 -0
  4. package/dist/api/codes.js +88 -0
  5. package/dist/cjs/analyze.d.cts +9 -0
  6. package/dist/cjs/api/agreement.d.cts +18 -0
  7. package/dist/cjs/api/codes.d.cts +6 -0
  8. package/dist/cjs/api/errors.d.cts +12 -0
  9. package/dist/cjs/api/guards.d.cts +4 -0
  10. package/dist/cjs/api/summary.d.cts +22 -0
  11. package/dist/cjs/check/index.d.cts +60 -0
  12. package/dist/cjs/check.cjs +1 -0
  13. package/dist/cjs/chunks/build-CT5g6_bs.cjs +1 -0
  14. package/dist/cjs/chunks/compare-D7RVqlqf.cjs +1 -0
  15. package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
  16. package/dist/cjs/chunks/fallback-D9Ak2zFv.cjs +1 -0
  17. package/dist/cjs/chunks/guards-Dqpjtkdn.cjs +1 -0
  18. package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
  19. package/dist/cjs/chunks/isobmff-B_bb5rS5.cjs +1 -0
  20. package/dist/cjs/chunks/matroska-CLoGzfKA.cjs +1 -0
  21. package/dist/cjs/chunks/probe-DDLUfr23.cjs +1 -0
  22. package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
  23. package/dist/cjs/chunks/reader-DNLR-_tm.cjs +1 -0
  24. package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
  25. package/dist/cjs/chunks/runtime-DtgT0xzU.cjs +1 -0
  26. package/dist/cjs/chunks/segments-i-hPN7H3.cjs +1 -0
  27. package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
  28. package/dist/cjs/chunks/stats-D68frkk2.cjs +1 -0
  29. package/dist/cjs/client/types.d.cts +96 -0
  30. package/dist/cjs/core/reader.d.cts +47 -0
  31. package/dist/cjs/core/types.d.cts +149 -0
  32. package/dist/cjs/engines/types.d.cts +69 -0
  33. package/dist/cjs/header/index.d.cts +25 -0
  34. package/dist/cjs/header/limits.d.cts +25 -0
  35. package/dist/cjs/header.cjs +1 -0
  36. package/dist/cjs/index.cjs +1 -0
  37. package/dist/cjs/index.d.cts +13 -0
  38. package/dist/cjs/plugins/types.d.cts +17 -0
  39. package/dist/cjs/presets/index.d.cts +19 -0
  40. package/dist/cjs/presets.cjs +1 -0
  41. package/dist/cjs/probe.d.cts +3 -0
  42. package/dist/cjs/url/index.d.cts +47 -0
  43. package/dist/cjs/url.cjs +1 -0
  44. package/dist/cjs/worker/protocol.d.cts +47 -0
  45. package/dist/core/dispatch.js +18 -29
  46. package/dist/core/isobmff/boxes.js +6 -5
  47. package/dist/core/isobmff/build.js +398 -0
  48. package/dist/core/isobmff/header.js +654 -0
  49. package/dist/core/isobmff/index.js +9 -381
  50. package/dist/core/isobmff/moof.js +2 -2
  51. package/dist/core/isobmff/moov.js +2 -2
  52. package/dist/core/matroska/header.js +9 -4
  53. package/dist/core/matroska/index.js +7 -3
  54. package/dist/core/reader.d.ts +3 -0
  55. package/dist/core/reader.js +13 -7
  56. package/dist/core/result.js +1 -0
  57. package/dist/core/stats.js +3 -0
  58. package/dist/core/types.d.ts +15 -1
  59. package/dist/engines/common.js +2 -0
  60. package/dist/header/index.d.ts +25 -0
  61. package/dist/header/index.js +146 -0
  62. package/dist/header/limits.d.ts +25 -0
  63. package/dist/header/limits.js +140 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +1 -0
  66. package/dist/standalone/own-isobmff.worker.js +1 -1
  67. package/dist/standalone/own-matroska.worker.js +1 -1
  68. package/dist/standalone/own.worker.js +1 -1
  69. package/dist/url/index.d.ts +47 -0
  70. package/dist/url/index.js +335 -0
  71. package/dist/worker/single.js +2 -1
  72. package/package.json +79 -2
@@ -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;
@@ -108,6 +108,7 @@ export function applyStats(track, stats, source = 'exact') {
108
108
  track.dominantFps = stats.dominantFps;
109
109
  track.minFps = stats.minFps;
110
110
  track.maxFps = stats.maxFps;
111
+ track.medianFrameInterval = stats.medianFrameInterval;
111
112
  track.isVfr = stats.isVfr;
112
113
  track.vfrRatio = stats.vfrRatio;
113
114
  track.distinctDeltas = stats.distinctDeltas;
@@ -126,5 +127,6 @@ export function applyStats(track, stats, source = 'exact') {
126
127
  maxFps: source,
127
128
  isVfr: source,
128
129
  duplicateTimestamps: source,
130
+ medianFrameInterval: source,
129
131
  });
130
132
  }
@@ -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';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { analyze } from "./analyze.js";
2
2
  export { compareResults } from "./api/agreement.js";
3
+ export { errorClass, errorCodeOf, warningCodeOf, warningCodesOf } from "./api/codes.js";
3
4
  export { ProbeError } from "./api/errors.js";
4
5
  export { isAbortError, isTimeoutError } from "./api/guards.js";
5
6
  export { summarize } from "./api/summary.js";