video-fps-probe 0.3.0 → 0.4.0-beta.2

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 (94) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +19 -11
  3. package/dist/analyze.js +10 -1
  4. package/dist/api/codes.d.ts +6 -0
  5. package/dist/api/codes.js +90 -0
  6. package/dist/aspect/index.d.ts +13 -0
  7. package/dist/aspect/index.js +180 -0
  8. package/dist/aspect/rational.js +61 -0
  9. package/dist/cjs/analyze.d.cts +9 -0
  10. package/dist/cjs/api/agreement.d.cts +18 -0
  11. package/dist/cjs/api/codes.d.cts +6 -0
  12. package/dist/cjs/api/errors.d.cts +12 -0
  13. package/dist/cjs/api/guards.d.cts +4 -0
  14. package/dist/cjs/api/summary.d.cts +22 -0
  15. package/dist/cjs/aspect/index.d.cts +13 -0
  16. package/dist/cjs/aspect.cjs +1 -0
  17. package/dist/cjs/check/index.d.cts +60 -0
  18. package/dist/cjs/check.cjs +1 -0
  19. package/dist/cjs/chunks/boxes-B0T2nt3e.cjs +1 -0
  20. package/dist/cjs/chunks/build-DhETwJLO.cjs +1 -0
  21. package/dist/cjs/chunks/color-xqJHSfhZ.cjs +1 -0
  22. package/dist/cjs/chunks/compare-B_HKNwqg.cjs +1 -0
  23. package/dist/cjs/chunks/config-CSbXzEqr.cjs +1 -0
  24. package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
  25. package/dist/cjs/chunks/fallback-CyPIR3RM.cjs +1 -0
  26. package/dist/cjs/chunks/guards-DbQNcAZS.cjs +1 -0
  27. package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
  28. package/dist/cjs/chunks/isobmff-4RrRf9N-.cjs +1 -0
  29. package/dist/cjs/chunks/matroska-BhgWtuPU.cjs +1 -0
  30. package/dist/cjs/chunks/metadata-ECdzNLkE.cjs +1 -0
  31. package/dist/cjs/chunks/options-B_vLHxHd.cjs +1 -0
  32. package/dist/cjs/chunks/probe-Cn1DtL-C.cjs +1 -0
  33. package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
  34. package/dist/cjs/chunks/reader-Dw11ozVU.cjs +1 -0
  35. package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
  36. package/dist/cjs/chunks/runtime-BgAlQQee.cjs +1 -0
  37. package/dist/cjs/chunks/segments-B1H6bDz-.cjs +1 -0
  38. package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
  39. package/dist/cjs/chunks/stats-DW7dVdFa.cjs +1 -0
  40. package/dist/cjs/client/types.d.cts +96 -0
  41. package/dist/cjs/color/index.d.cts +7 -0
  42. package/dist/cjs/color.cjs +1 -0
  43. package/dist/cjs/core/reader.d.cts +47 -0
  44. package/dist/cjs/core/types.d.cts +194 -0
  45. package/dist/cjs/engines/types.d.cts +69 -0
  46. package/dist/cjs/header/index.d.cts +28 -0
  47. package/dist/cjs/header/limits.d.cts +26 -0
  48. package/dist/cjs/header.cjs +1 -0
  49. package/dist/cjs/index.cjs +1 -0
  50. package/dist/cjs/index.d.cts +13 -0
  51. package/dist/cjs/plugins/types.d.cts +17 -0
  52. package/dist/cjs/presets/index.d.cts +19 -0
  53. package/dist/cjs/presets.cjs +1 -0
  54. package/dist/cjs/probe.d.cts +3 -0
  55. package/dist/cjs/url/index.d.cts +47 -0
  56. package/dist/cjs/url.cjs +1 -0
  57. package/dist/cjs/worker/protocol.d.cts +47 -0
  58. package/dist/color/codec-string.js +121 -0
  59. package/dist/color/config.js +199 -0
  60. package/dist/color/index.d.ts +7 -0
  61. package/dist/color/index.js +139 -0
  62. package/dist/color/names.js +33 -0
  63. package/dist/color/sps.js +275 -0
  64. package/dist/core/dispatch.js +18 -29
  65. package/dist/core/isobmff/boxes.js +6 -5
  66. package/dist/core/isobmff/build.js +401 -0
  67. package/dist/core/isobmff/config.js +34 -0
  68. package/dist/core/isobmff/header.js +677 -0
  69. package/dist/core/isobmff/index.js +9 -381
  70. package/dist/core/isobmff/moof.js +2 -2
  71. package/dist/core/isobmff/moov.js +3 -3
  72. package/dist/core/matroska/header.js +10 -4
  73. package/dist/core/matroska/index.js +11 -5
  74. package/dist/core/options.js +1 -0
  75. package/dist/core/reader.d.ts +3 -0
  76. package/dist/core/reader.js +13 -7
  77. package/dist/core/result.js +1 -0
  78. package/dist/core/stats.js +3 -0
  79. package/dist/core/types.d.ts +60 -1
  80. package/dist/engines/common.js +2 -0
  81. package/dist/header/index.d.ts +28 -0
  82. package/dist/header/index.js +182 -0
  83. package/dist/header/limits.d.ts +26 -0
  84. package/dist/header/limits.js +144 -0
  85. package/dist/header/metadata.js +2 -0
  86. package/dist/index.d.ts +1 -0
  87. package/dist/index.js +1 -0
  88. package/dist/standalone/own-isobmff.worker.js +1 -1
  89. package/dist/standalone/own-matroska.worker.js +1 -1
  90. package/dist/standalone/own.worker.js +1 -1
  91. package/dist/url/index.d.ts +47 -0
  92. package/dist/url/index.js +335 -0
  93. package/dist/worker/single.js +2 -1
  94. package/package.json +117 -2
@@ -38,6 +38,7 @@ export function emptyTrack(trackId) {
38
38
  timestampsQuantized: false,
39
39
  isStill: false,
40
40
  rateProfile: null,
41
+ medianFrameInterval: null,
41
42
  provenance: {},
42
43
  warnings: [],
43
44
  };
@@ -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);
@@ -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,14 +74,69 @@ export interface VideoTrackResult {
74
74
  timestampsQuantized: boolean;
75
75
  isStill: boolean;
76
76
  rateProfile?: RateProfile | null;
77
+ medianFrameInterval?: Rational | null;
78
+ enabled?: boolean;
79
+ codecConfig?: CodecConfig;
80
+ color?: ColorInfo | null;
81
+ sampleAspectRatio?: Rational | null;
82
+ displayAspectRatio?: Rational | null;
83
+ displayWidth?: number | null;
84
+ displayHeight?: number | null;
77
85
  provenance: Partial<Record<TrackField, Provenance>>;
78
86
  warnings: string[];
79
87
  }
88
+ export type CodecConfig = {
89
+ family: 'isobmff';
90
+ stsd: Uint8Array | null;
91
+ tkhd: Uint8Array | null;
92
+ mvhd: Uint8Array | null;
93
+ } | {
94
+ family: 'isobmff-moov';
95
+ moov: DataView;
96
+ trak: number;
97
+ } | {
98
+ family: 'matroska';
99
+ trackEntry: Uint8Array | null;
100
+ };
101
+ export type ColorSource = 'sps' | 'colr' | 'vpcC' | 'Colour';
102
+ export interface DolbyVisionConfig {
103
+ versionMajor: number;
104
+ versionMinor: number;
105
+ profile: number;
106
+ level: number;
107
+ rpu: boolean;
108
+ el: boolean;
109
+ bl: boolean;
110
+ compatibilityId: number;
111
+ mdCompression: number;
112
+ }
113
+ export interface ColorInfo {
114
+ range: 'tv' | 'pc' | null;
115
+ primaries: string | null;
116
+ transfer: string | null;
117
+ space: string | null;
118
+ source: ColorSource | null;
119
+ bitDepth: number | null;
120
+ profile: number | null;
121
+ codecString: string | null;
122
+ hdr: 'pq' | 'hlg' | null;
123
+ dolbyVision: DolbyVisionConfig | null;
124
+ }
80
125
  export interface IoStats {
81
126
  reads: number;
82
127
  bytesRead: number;
83
128
  ms: number;
84
129
  }
130
+ 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';
131
+ export type LayoutKind = 'faststart' | 'moov-at-end' | 'fragmented';
132
+ export interface ContainerLayout {
133
+ kind: LayoutKind;
134
+ moovOffset: number | null;
135
+ moovSize: number | null;
136
+ sidx: boolean;
137
+ emptyMoov: boolean;
138
+ }
139
+ export type WarningCode = 'invalid-timescale' | 'truncated-mdat' | 'truncated-box' | 'trailing-data' | 'fragment-index-unread' | 'duration-unresolved' | 'fragments-ignored' | 'structure-limit' | 'codec-config-unavailable' | 'metadata-unavailable';
85
140
  export interface FileResult {
86
141
  name: string;
87
142
  size: number;
@@ -93,6 +148,8 @@ export interface FileResult {
93
148
  audioTrackCount: number;
94
149
  primaryTrackId: number | null;
95
150
  error: string | null;
151
+ errorCode?: FileErrorCode | null;
152
+ layout?: ContainerLayout | null;
96
153
  warnings: string[];
97
154
  io: IoStats;
98
155
  engine: EngineId;
@@ -122,6 +179,7 @@ export interface AnalyzeOptions {
122
179
  costModel?: Partial<CostModel>;
123
180
  maxExpandedSamples?: number;
124
181
  onProgress?: ((progress: AnalyzeProgress) => void) | null;
182
+ codecConfig?: boolean;
125
183
  }
126
184
  export interface ResolvedAnalyzeOptions {
127
185
  signal: AbortSignal | null;
@@ -131,5 +189,6 @@ export interface ResolvedAnalyzeOptions {
131
189
  costModel: CostModel;
132
190
  maxExpandedSamples: number;
133
191
  onProgress: ((progress: AnalyzeProgress) => void) | null;
192
+ codecConfig?: boolean;
134
193
  }
135
194
  export type ContainerAnalyzer = (reader: BlobReader, opts: ResolvedAnalyzeOptions) => Promise<ContainerAnalysis>;
@@ -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,28 @@
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
+ codecConfig?: boolean;
20
+ color?: boolean;
21
+ aspect?: boolean;
22
+ }
23
+ export type HeaderResult = FileResult & {
24
+ io: IoStats & {
25
+ bytesRequested: number;
26
+ };
27
+ };
28
+ export declare function analyzeHeader(source: Blob | BlobLike, options?: HeaderOptions): Promise<HeaderResult>;
@@ -0,0 +1,182 @@
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 flag(value, what) {
37
+ if (value !== undefined && typeof value !== 'boolean')
38
+ throw new TypeError(`${what} must be a boolean, got ${shown(value)}`);
39
+ return value === true;
40
+ }
41
+ function resolveHeaderOptions(source, options) {
42
+ const src = source;
43
+ if (typeof src !== 'object' || src === null || typeof src.slice !== 'function')
44
+ throw new TypeError(`source must be a Blob or a BlobLike with a size and a slice method, got ${shown(source)}`);
45
+ const size = count(src.size, 'source.size');
46
+ const o = record(options, 'options');
47
+ if (o.name !== undefined && typeof o.name !== 'string')
48
+ throw new TypeError(`name must be a string, got ${shown(o.name)}`);
49
+ const signal = o.signal ?? null;
50
+ if (signal !== null) {
51
+ const s = signal;
52
+ if (typeof s !== 'object' || typeof s.aborted !== 'boolean' || typeof s.addEventListener !== 'function' || typeof s.removeEventListener !== 'function')
53
+ throw new TypeError(`signal must be an AbortSignal, got ${shown(signal)}`);
54
+ }
55
+ const l = record(o.limits, 'limits');
56
+ const limits = { ...HEADER_DEFAULT_LIMITS };
57
+ for (const key of Object.keys(HEADER_DEFAULT_LIMITS))
58
+ if (l[key] !== undefined)
59
+ limits[key] = count(l[key], `limits.${key}`, 0, true);
60
+ const r = record(o.readAhead, 'readAhead');
61
+ for (const key of Object.keys(r))
62
+ if (key !== 'head' && key !== 'tail' && r[key] !== undefined)
63
+ throw new TypeError(`readAhead takes only head and tail, got ${key}`);
64
+ const head = r.head === undefined ? DEFAULT_READ_AHEAD.head : count(r.head, 'readAhead.head', MIN_HEAD);
65
+ const tail = r.tail === undefined ? DEFAULT_READ_AHEAD.tail : count(r.tail, 'readAhead.tail', MIN_TAIL);
66
+ const mode = o.fragmentedDuration ?? 'index';
67
+ if (mode !== 'index' && mode !== 'header')
68
+ throw new TypeError(`fragmentedDuration must be 'header' or 'index', got ${shown(mode)}`);
69
+ const name = o.name ?? (typeof File !== 'undefined' && source instanceof File ? source.name : '');
70
+ return { name, size, signal: signal, limits, head, tail, index: mode === 'index', codecConfig: flag(o.codecConfig, 'codecConfig'), color: flag(o.color, 'color'), aspect: flag(o.aspect, 'aspect') };
71
+ }
72
+ function docTypeOf(head) {
73
+ let p = 4;
74
+ const vint = (marker) => {
75
+ if (p >= head.length)
76
+ return null;
77
+ const first = head[p];
78
+ let len = 1;
79
+ while (len <= 8 && !(first & (0x80 >> (len - 1))))
80
+ len++;
81
+ if (len > 8 || p + len > head.length)
82
+ return null;
83
+ let v = marker ? first : first & (0xff >> len);
84
+ for (let i = 1; i < len; i++)
85
+ v = v * 256 + head[p + i];
86
+ p += len;
87
+ return v;
88
+ };
89
+ const size = vint(false);
90
+ if (size === null)
91
+ return null;
92
+ const end = Math.min(head.length, p + size);
93
+ while (p < end) {
94
+ const id = vint(true);
95
+ const n = vint(false);
96
+ if (id === null || n === null || p + n > end)
97
+ return null;
98
+ if (id === 0x4282)
99
+ return String.fromCharCode(...head.subarray(p, p + n)).replace(/\0+$/, '');
100
+ p += n;
101
+ }
102
+ return null;
103
+ }
104
+ function failed(analysis, message, code) {
105
+ analysis.error = message;
106
+ analysis.errorCode = code;
107
+ return analysis;
108
+ }
109
+ async function analyzeSource(reader, limited, o) {
110
+ const analysis = { ...emptyAnalysis('unknown'), scan: 'header', errorCode: null, layout: null };
111
+ const size = reader.size;
112
+ if (size === 0)
113
+ return failed(analysis, 'not a recognised container: empty file', 'empty-file');
114
+ const head = await reader.bytes(0, Math.min(SNIFF_BYTES, size), Math.min(size, o.head, limited.bytesLeft));
115
+ const sniffed = sniff(head);
116
+ if (sniffed.kind === 'unknown')
117
+ return failed(analysis, `not a recognised container: ${sniffed.reason}`, 'unrecognised');
118
+ if (sniffed.kind === 'matroska') {
119
+ analysis.container = docTypeOf(head) === 'webm' ? 'webm' : 'matroska';
120
+ return failed(analysis, `container not read by the header entry: ${analysis.container}`, 'unsupported-container');
121
+ }
122
+ const { maxTracks, maxTopLevelBoxes } = o.limits;
123
+ return analyzeIsoBmffHeader(reader, limited, { head: o.head, tail: o.tail, maxTracks, maxTopLevelBoxes, index: o.index, codecConfig: o.codecConfig || o.color || o.aspect });
124
+ }
125
+ const messageOf = (e) => (e instanceof Error ? e.message || e.name : String(e));
126
+ let metadata;
127
+ let metadataLoad;
128
+ function loadMetadata() {
129
+ metadataLoad ??= import("./metadata.js").then((m) => (metadata = m), (e) => {
130
+ metadataLoad = undefined;
131
+ throw e;
132
+ });
133
+ return metadataLoad;
134
+ }
135
+ async function withMetadata(analysis, o, limited) {
136
+ try {
137
+ const { withAspect, withColor } = metadata ?? (await limited.race(loadMetadata()));
138
+ throwIfAborted(o.signal);
139
+ const last = { keepCodecConfig: o.codecConfig };
140
+ const colored = o.color ? withColor(analysis, o.aspect ? { keepCodecConfig: true } : last) : analysis;
141
+ return o.aspect ? withAspect(colored, last) : colored;
142
+ }
143
+ catch (e) {
144
+ throwIfAborted(o.signal);
145
+ const videoTracks = analysis.videoTracks.map((t) => {
146
+ const kept = { ...t };
147
+ if (!o.codecConfig)
148
+ delete kept.codecConfig;
149
+ return kept;
150
+ });
151
+ return { ...analysis, videoTracks, warnings: [...analysis.warnings, `color and aspect unavailable: ${messageOf(e)}`] };
152
+ }
153
+ }
154
+ export async function analyzeHeader(source, options = {}) {
155
+ const t0 = performance.now();
156
+ const o = resolveHeaderOptions(source, options);
157
+ throwIfAborted(o.signal);
158
+ const limited = new LimitedSource(source, o.size, o.limits, o.signal);
159
+ const reader = new BlobReader(limited, { headWindow: o.head, signal: o.signal });
160
+ let analysis;
161
+ try {
162
+ analysis = await analyzeSource(reader, limited, o);
163
+ if ((o.color || o.aspect) && analysis.videoTracks.some((t) => t.codecConfig))
164
+ analysis = await withMetadata(analysis, o, limited);
165
+ }
166
+ catch (e) {
167
+ throwIfAborted(o.signal);
168
+ const limit = limited.take();
169
+ analysis = { ...emptyAnalysis('unknown'), scan: 'header', errorCode: null, layout: null };
170
+ failed(analysis, limit ? limit.message : messageOf(e), whichErrorCode(limit ?? e));
171
+ }
172
+ finally {
173
+ limited.dispose();
174
+ }
175
+ return {
176
+ name: o.name,
177
+ size: o.size,
178
+ ...analysis,
179
+ io: { reads: reader.reads, bytesRead: reader.bytesFetched, ms: performance.now() - t0, bytesRequested: limited.bytesRequested },
180
+ engine: 'own',
181
+ };
182
+ }
@@ -0,0 +1,26 @@
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
+ race<T>(pending: Promise<T>): Promise<T>;
24
+ slice(start?: number, end?: number): BlobSlice;
25
+ dispose(): void;
26
+ }
@@ -0,0 +1,144 @@
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
+ race(pending) {
91
+ pending.catch(noop);
92
+ return Promise.race([pending, this.#stopped]);
93
+ }
94
+ slice(start = 0, end = this.size) {
95
+ return { arrayBuffer: () => this.#fetch(start, end) };
96
+ }
97
+ dispose() {
98
+ if (this.#timer !== null)
99
+ clearTimeout(this.#timer);
100
+ this.#timer = null;
101
+ this.#caller?.removeEventListener('abort', this.#forward);
102
+ }
103
+ #trip(code, message) {
104
+ this.#tripped = new LimitError(code, message);
105
+ return this.#tripped;
106
+ }
107
+ #admission(bytes, offset) {
108
+ if (this.#reads + 1 > this.#maxReads)
109
+ return this.#trip('read-limit', `limit reached: maxReads ${this.#maxReads}`);
110
+ if (this.#bytes + bytes > this.#maxBytes)
111
+ return this.#trip('byte-limit', `limit reached: maxBytes ${this.#maxBytes} (${this.#bytes + bytes - this.#maxBytes} more bytes needed at offset ${offset})`);
112
+ try {
113
+ this.checkTime();
114
+ }
115
+ catch (e) {
116
+ return e;
117
+ }
118
+ return null;
119
+ }
120
+ #fetch(start, end) {
121
+ const length = Math.max(0, end - start);
122
+ const trip = this.#admission(length, start);
123
+ if (trip)
124
+ return Promise.reject(trip);
125
+ this.#reads++;
126
+ this.#bytes += length;
127
+ let pending;
128
+ try {
129
+ const inner = this.#inner;
130
+ pending = Promise.resolve(typeof inner.read === 'function' ? inner.read(start, end, this.#controller.signal) : inner.slice(start, end).arrayBuffer()).then(bufferOf);
131
+ }
132
+ catch (e) {
133
+ pending = Promise.reject(e);
134
+ }
135
+ pending.catch(noop);
136
+ return Promise.race([pending, this.#stopped]).catch((e) => {
137
+ if (e instanceof LimitError)
138
+ this.#tripped = e;
139
+ else if (!this.#controller.signal.aborted && e?.name === 'AbortError')
140
+ throw sourceError(String(e));
141
+ throw e;
142
+ });
143
+ }
144
+ }
@@ -0,0 +1,2 @@
1
+ export { withAspect } from "../aspect/index.js";
2
+ export { withColor } from "../color/index.js";
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";