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.
- package/CHANGELOG.md +72 -0
- package/README.md +19 -11
- package/dist/analyze.js +10 -1
- package/dist/api/codes.d.ts +6 -0
- package/dist/api/codes.js +90 -0
- package/dist/aspect/index.d.ts +13 -0
- package/dist/aspect/index.js +180 -0
- package/dist/aspect/rational.js +61 -0
- 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/aspect/index.d.cts +13 -0
- package/dist/cjs/aspect.cjs +1 -0
- package/dist/cjs/check/index.d.cts +60 -0
- package/dist/cjs/check.cjs +1 -0
- package/dist/cjs/chunks/boxes-B0T2nt3e.cjs +1 -0
- package/dist/cjs/chunks/build-DhETwJLO.cjs +1 -0
- package/dist/cjs/chunks/color-xqJHSfhZ.cjs +1 -0
- package/dist/cjs/chunks/compare-B_HKNwqg.cjs +1 -0
- package/dist/cjs/chunks/config-CSbXzEqr.cjs +1 -0
- package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
- package/dist/cjs/chunks/fallback-CyPIR3RM.cjs +1 -0
- package/dist/cjs/chunks/guards-DbQNcAZS.cjs +1 -0
- package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
- package/dist/cjs/chunks/isobmff-4RrRf9N-.cjs +1 -0
- package/dist/cjs/chunks/matroska-BhgWtuPU.cjs +1 -0
- package/dist/cjs/chunks/metadata-ECdzNLkE.cjs +1 -0
- package/dist/cjs/chunks/options-B_vLHxHd.cjs +1 -0
- package/dist/cjs/chunks/probe-Cn1DtL-C.cjs +1 -0
- package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
- package/dist/cjs/chunks/reader-Dw11ozVU.cjs +1 -0
- package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
- package/dist/cjs/chunks/runtime-BgAlQQee.cjs +1 -0
- package/dist/cjs/chunks/segments-B1H6bDz-.cjs +1 -0
- package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
- package/dist/cjs/chunks/stats-DW7dVdFa.cjs +1 -0
- package/dist/cjs/client/types.d.cts +96 -0
- package/dist/cjs/color/index.d.cts +7 -0
- package/dist/cjs/color.cjs +1 -0
- package/dist/cjs/core/reader.d.cts +47 -0
- package/dist/cjs/core/types.d.cts +194 -0
- package/dist/cjs/engines/types.d.cts +69 -0
- package/dist/cjs/header/index.d.cts +28 -0
- package/dist/cjs/header/limits.d.cts +26 -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/color/codec-string.js +121 -0
- package/dist/color/config.js +199 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.js +139 -0
- package/dist/color/names.js +33 -0
- package/dist/color/sps.js +275 -0
- package/dist/core/dispatch.js +18 -29
- package/dist/core/isobmff/boxes.js +6 -5
- package/dist/core/isobmff/build.js +401 -0
- package/dist/core/isobmff/config.js +34 -0
- package/dist/core/isobmff/header.js +677 -0
- package/dist/core/isobmff/index.js +9 -381
- package/dist/core/isobmff/moof.js +2 -2
- package/dist/core/isobmff/moov.js +3 -3
- package/dist/core/matroska/header.js +10 -4
- package/dist/core/matroska/index.js +11 -5
- package/dist/core/options.js +1 -0
- package/dist/core/reader.d.ts +3 -0
- package/dist/core/reader.js +13 -7
- package/dist/core/result.js +1 -0
- package/dist/core/stats.js +3 -0
- package/dist/core/types.d.ts +60 -1
- package/dist/engines/common.js +2 -0
- package/dist/header/index.d.ts +28 -0
- package/dist/header/index.js +182 -0
- package/dist/header/limits.d.ts +26 -0
- package/dist/header/limits.js +144 -0
- package/dist/header/metadata.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/standalone/own-isobmff.worker.js +1 -1
- package/dist/standalone/own-matroska.worker.js +1 -1
- 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/single.js +2 -1
- package/package.json +117 -2
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,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>;
|
package/dist/engines/common.js
CHANGED
|
@@ -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
|
+
}
|
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";
|