video-fps-probe 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/LICENSE +21 -0
  3. package/README.md +182 -2
  4. package/dist/analyze.d.ts +9 -0
  5. package/dist/analyze.js +5 -0
  6. package/dist/api/agreement.d.ts +18 -0
  7. package/dist/api/agreement.js +55 -0
  8. package/dist/api/errors.d.ts +12 -0
  9. package/dist/api/errors.js +10 -0
  10. package/dist/api/guards.d.ts +4 -0
  11. package/dist/api/guards.js +9 -0
  12. package/dist/api/summary.d.ts +22 -0
  13. package/dist/api/summary.js +25 -0
  14. package/dist/check/index.d.ts +60 -0
  15. package/dist/check/index.js +281 -0
  16. package/dist/client/compare.js +57 -0
  17. package/dist/client/create.d.ts +3 -0
  18. package/dist/client/create.js +8 -0
  19. package/dist/client/fallback.js +43 -0
  20. package/dist/client/index.d.ts +11 -0
  21. package/dist/client/index.js +5 -0
  22. package/dist/client/inline.js +43 -0
  23. package/dist/client/lazy.d.ts +6 -0
  24. package/dist/client/lazy.js +7 -0
  25. package/dist/client/pool.js +174 -0
  26. package/dist/client/probe-client.js +4 -0
  27. package/dist/client/probe.js +132 -0
  28. package/dist/client/route.js +20 -0
  29. package/dist/client/split.js +9 -0
  30. package/dist/client/stub.js +45 -0
  31. package/dist/client/types.d.ts +96 -0
  32. package/dist/client/types.js +1 -0
  33. package/dist/core/abort.js +4 -0
  34. package/dist/core/analyze.js +13 -0
  35. package/dist/core/dispatch.js +76 -0
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/isobmff/boxes.js +73 -0
  38. package/dist/core/isobmff/codecs.js +43 -0
  39. package/dist/core/isobmff/index.js +424 -0
  40. package/dist/core/isobmff/model.js +60 -0
  41. package/dist/core/isobmff/moof.js +232 -0
  42. package/dist/core/isobmff/moov.js +346 -0
  43. package/dist/core/isobmff/prefetch.js +90 -0
  44. package/dist/core/isobmff/track.js +546 -0
  45. package/dist/core/matroska/ebml.js +164 -0
  46. package/dist/core/matroska/header.js +380 -0
  47. package/dist/core/matroska/index.js +466 -0
  48. package/dist/core/matroska/scan.js +419 -0
  49. package/dist/core/matroska/source.js +145 -0
  50. package/dist/core/options.js +38 -0
  51. package/dist/core/rational.js +101 -0
  52. package/dist/core/reader.d.ts +44 -0
  53. package/dist/core/reader.js +200 -0
  54. package/dist/core/result.js +49 -0
  55. package/dist/core/rfps.js +438 -0
  56. package/dist/core/segments.js +234 -0
  57. package/dist/core/sniff.js +99 -0
  58. package/dist/core/stats.js +622 -0
  59. package/dist/core/types.d.ts +135 -0
  60. package/dist/core/types.js +1 -0
  61. package/dist/engines/common.js +130 -0
  62. package/dist/engines/counting.js +78 -0
  63. package/dist/engines/mediabunny.d.ts +9 -0
  64. package/dist/engines/mediabunny.js +254 -0
  65. package/dist/engines/mp4box.d.ts +8 -0
  66. package/dist/engines/mp4box.js +430 -0
  67. package/dist/engines/rvfc.js +216 -0
  68. package/dist/engines/types.d.ts +69 -0
  69. package/dist/engines/types.js +1 -0
  70. package/dist/index.d.ts +12 -0
  71. package/dist/index.js +6 -0
  72. package/dist/lazy.d.ts +6 -0
  73. package/dist/lazy.js +6 -0
  74. package/dist/plugins/mediabunny.d.ts +2 -0
  75. package/dist/plugins/mediabunny.js +9 -0
  76. package/dist/plugins/mp4box.d.ts +2 -0
  77. package/dist/plugins/mp4box.js +9 -0
  78. package/dist/plugins/own.js +9 -0
  79. package/dist/plugins/rvfc.d.ts +5 -0
  80. package/dist/plugins/rvfc.js +3 -0
  81. package/dist/plugins/types.d.ts +17 -0
  82. package/dist/plugins/types.js +1 -0
  83. package/dist/presets/index.d.ts +19 -0
  84. package/dist/presets/index.js +54 -0
  85. package/dist/presets/sources.json +8 -0
  86. package/dist/probe.d.ts +3 -0
  87. package/dist/probe.js +7 -0
  88. package/dist/react/index.d.ts +14 -0
  89. package/dist/react/index.js +30 -0
  90. package/dist/react/store.d.ts +25 -0
  91. package/dist/react/store.js +95 -0
  92. package/dist/standalone/own-isobmff.worker.js +1 -0
  93. package/dist/standalone/own-matroska.worker.js +1 -0
  94. package/dist/standalone/own.worker.js +1 -0
  95. package/dist/worker/mediabunny.worker.js +5 -0
  96. package/dist/worker/mp4box.worker.js +5 -0
  97. package/dist/worker/own-isobmff.worker.js +4 -0
  98. package/dist/worker/own-matroska.worker.js +4 -0
  99. package/dist/worker/own.worker.js +2 -0
  100. package/dist/worker/protocol.d.ts +47 -0
  101. package/dist/worker/protocol.js +1 -0
  102. package/dist/worker/runtime.d.ts +3 -0
  103. package/dist/worker/runtime.js +9 -0
  104. package/dist/worker/serve.d.ts +3 -0
  105. package/dist/worker/serve.js +97 -0
  106. package/dist/worker/single.js +13 -0
  107. package/package.json +207 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,130 @@
1
+ # Changelog
2
+
3
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow semver with the [0.x policy](docs/api.md#versioning).
4
+
5
+ ## Unreleased
6
+
7
+ ## 0.3.0 - 2026-10-30
8
+
9
+ ### Added
10
+
11
+ - `byContainer` on a custom worker: `{ create, engines, byContainer: { matroska: () => new Worker(...) } }` runs own on the worker of the file's container family, and every other engine and family on `create` ([api](docs/api.md#createprobeclient-client-flavor)).
12
+ - `video-fps-probe/standalone/own-isobmff.worker.js` and `own-matroska.worker.js`: the two own workers as classic scripts with no imports, about 21.1 and 23.7 KB gzip, for `byContainer`. The combined `own.worker.js` stays.
13
+ - `video-fps-probe/presets/sources.json`: the source page of every preset, keyed by the name of its export, such as `sources.klingO1ReferenceVideo` ([presets](docs/api.md#presets)).
14
+ - CommonJS is tested from the packed tarball: `require()` on Node 22.12 and 24, a TypeScript `commonjs` and `node10` project as in NestJS 11, and Jest 30 with babel-jest ([Jest](docs/api.md#jest)).
15
+ - Issue templates for bugs and feature requests, and GitHub Discussions for questions and for showing how you use the package ([feedback](README.md#feedback)).
16
+
17
+ ### Changed
18
+
19
+ - **Behaviour change:** own runs on one worker per container family, `own-isobmff.worker` or `own-matroska.worker`, picked by the file's first 4 bytes, so a first file loads only the parser of its family.
20
+ - The first MP4 loads about 22.7 KB gzip (23.6 KB in 0.2.0) and the first WebM or MKV without DefaultDuration 25.5 KB (26.7 KB). With Vite's default worker format they load 24.5 and 27.1 KB instead of 38.9 KB ([size matrix](report/size-matrix.md)).
21
+ - `workers` counts per worker kind, so `workers: 1` can run one MP4 and one WebM at once. `prewarm()` starts the worker of each container family it is given, ISO BMFF by default.
22
+ - A missing own worker file, such as a 404 after a redeploy, rejects only the jobs of its container family with `reason: 'load'`, and the next call retries.
23
+ - Service worker precache lists that name `own.worker` need both new worker files. A custom worker that runs `serveProbeWorker({ own: ownEngine })` from `video-fps-probe/worker-runtime` keeps the 0.2.0 layout.
24
+ - `lazyProbeClient` and `createProbeClient` load the 0.4 KB routing by container family only with `byContainer`, so a custom worker without it loads 23.6 KB for the first MP4 (23.5 KB in 0.2.0).
25
+ - The package is about 569 KB unpacked (442 KB in 0.2.0), mostly the two new standalone files, which a bundler takes only when imported.
26
+ - Still 0 mismatches against ffprobe 8.1.1, now on 130 committed fixtures ([accuracy](docs/benchmarks.md#accuracy-vs-ffprobe)).
27
+ - CI also tests Node 22.12, the lowest supported version. A weekly canary builds with the latest mp4box, mediabunny, Vite and rolldown and fails when an own result changes, an own size budget is exceeded or the build breaks, not when only mp4box or mediabunny code grows.
28
+
29
+ ### Deprecated
30
+
31
+ - `sourceUrl` of a preset: use `video-fps-probe/presets/sources.json`. Presets keep it until the next major, which leaves the URL strings that supply-chain scanners flag in `sources.json` only.
32
+
33
+ ### Fixed
34
+
35
+ - A version 1 `mehd` or `elst` holding 0xFFFFFFFFFFFFFFFF, an unknown duration, failed the whole file. `mehd` is now ignored, as ffprobe does, and such an edit lasts to the end of the media, with a warning.
36
+ - A top-level header after the moov that runs past the end of the file, such as the trailer of some phone MOV files, marked the file truncated. Unless it is an `mdat` or a `moof`, it now ends the walk with a warning, and samples past the end are still cut.
37
+ - Warnings could hold raw NUL and other control characters from the file, such as a box type. Every engine now replaces them with `?`.
38
+ - An unreadable file, such as a picked file deleted since (`NotReadableError`), now gives `file read failed: <error>` for every container and engine, not `corrupt file: ...` or the browser's bare message.
39
+ - A `ProbeError` from another copy of the package, such as one from an engine module or a custom probe given to `VideoFpsProvider`, became `worker-failed`. It now keeps its code everywhere ([errors](docs/api.md#errors)).
40
+ - `check` judged the `duration` of a VFR WebM or Matroska track by its measured span, up to one frame shorter than ffprobe's. It now judges `formatDurationSec` when it is within one typical frame interval of the span ([check](docs/api.md#check)).
41
+
42
+ ### Known limitations
43
+
44
+ - React 18 and 19 only: next to another React major npm refuses the install with `ERESOLVE` unless you pass `--legacy-peer-deps` or add `"overrides": { "video-fps-probe": { "react": "$react" } }`.
45
+ - Zero-config Vite (iife workers) builds each own worker as one file, so the first MP4 loads about 1.7 KB more than with `worker: { format: 'es' }`.
46
+ - Only Vite 8 and npm are tested in CI ([bundlers](docs/bundlers.md)). Browser floors (Chrome and Edge 94, Firefox 114, Safari 15.4) are derived, not tested.
47
+ - A TypeScript project for Node needs `"DOM"` in `lib` or `skipLibCheck: true`, and Jest needs babel-jest to transform the package ([Jest](docs/api.md#jest)).
48
+ - For a WebM or Matroska duration `check` takes the interval of `dominantFps`, `nominalFps` or `avgFps`, so a recording without a dominant rate uses its average interval, not its median.
49
+ - `check` judges the stored frame size: a `pasp` box or a Matroska DisplayWidth is not applied.
50
+ - The only real recordings in the corpus come from Chromium's MediaRecorder. More in [benchmarks](docs/benchmarks.md#limitations).
51
+
52
+ ### Not yet
53
+
54
+ - 0.3.x: a bundler matrix in CI: webpack 5, Rspack, Next.js, Angular CLI, SvelteKit, Nuxt, Parcel, Bun and Deno, with pnpm, Yarn PnP and bun installs.
55
+ - 0.4.0: a header-only ISO BMFF mode with byte, read and time limits, machine-readable error codes, a layout field, a Range source for object storage (a fetcher, no SDK), color, HDR and Dolby Vision basics, PAR and display size, a median frame interval for `check` and CommonJS server entries only.
56
+ - 0.5.0: a Node entry (`fromPath`, DOM-free types, a `worker_threads` pool), NestJS validators, a CLI, bitrate, GOP and audio details, cadence detection, Vue, Svelte and Angular adapters, and later MPEG-TS and HLS.
57
+
58
+ ## 0.2.0 - 2026-10-25
59
+
60
+ ### Added
61
+
62
+ - First release on npm: `npm install video-fps-probe`; Yarn, pnpm and bun install the same publish ([Publishing](docs/publishing.md)).
63
+ - `video-fps-probe/react`: `VideoFpsProvider`, `useVideoFps(file)`, `useVideoFpsProbe()` and `usePrewarmVideoFps()` for React 18 and 19. The hook caches results per file, runs one analysis under `StrictMode` and aborts it when its last component unmounts ([React](docs/api.md#react)).
64
+ - `video-fps-probe/check`: `check(result, rules)` judges a result against upload limits before the upload: frame rate, duration, frame size, aspect ratio, pixels, file size, container, codec, audio, VFR and uneven pacing. The rules are plain JSON ([Check](docs/api.md#check)).
65
+ - `check` compares exact rationals, so 23.976 fps is below `min: 24`, and an NTSC decimal such as 29.97 also matches its x/1001 rate. A missing, failed or malformed result is `unknown` where it lacks a value, never an exception, so an upload can fail open; a broken rule throws a `TypeError` naming it.
66
+ - The `check` report has a verdict, one violation per broken rule with a readable message, and a `suggestedFps` to convert to: a clean 1-in-k frame drop when the file is at least 1.5 times the nearest allowed rate (50 fps under `max: 30` gets 25), else the nearest allowed rate (31 fps gets 30).
67
+ - `video-fps-probe/presets`: the published input limits of six video generation APIs as `check` rules, with sources, verified on 2026-10-25: Kling O1 and Motion Control 3.0, Runway Aleph 2.0, Veo 3.1, and Wan 2.6 on fal and Seedance 2.0 on EvoLink, which only warn ([Presets](docs/api.md#presets)).
68
+ - `ProbeError.reason`: `'load'` when code did not load, such as a chunk that is gone after a redeploy ([deploy skew](docs/bundlers.md#deploy-skew)), `'crash'` when it failed after loading. The code stays `worker-failed`.
69
+ - `avgFpsFfprobe` and `rFpsFfprobe` for a WebM or Matroska track without DefaultDuration, such as a MediaRecorder recording: ffprobe's estimate for VP8, VP9, AV1 and Baseline H.264 in the [listed cases](docs/implementation-notes.md#ffprobe-frame-rates-without-defaultduration-020), else null with a warning.
70
+
71
+ ### Changed
72
+
73
+ - **Behaviour change:** a lazy stub whose probe client fails to load rejects with `ProbeError('worker-failed')`, `reason: 'load'` and the import error as `cause`, instead of the browser's import error. Code that caught that error should test `e.reason === 'load'`.
74
+ - `isAbortError` and `isTimeoutError` check the error's name and code instead of `instanceof`, so they also recognize a `ProbeError` from another copy of the package.
75
+ - A file smaller than 465,536 bytes is read whole in one read, so a small MP4 with its moov at the end takes 1 read instead of 2; that read is at most 1 MiB whatever the cost model.
76
+ - In `auto` mode the exact scan of a Matroska track without DefaultDuration also feeds the ffprobe estimate, so the first Clusters are read once; the track's warning says whether the estimate was reproduced.
77
+ - The mediabunny engine is verified with mediabunny 1.62.1; the peer range stays `^1.62.0`.
78
+ - Sizes (gzip): the first MP4 of the default configuration loads about 23.6 KB (23.3 KB in 0.1.0) and the critical path stays under 0.7 KB. The React entry adds about 1.2 KB, `check` about 3.9 KB and one preset about 0.5 KB, each only when imported ([size matrix](report/size-matrix.md)).
79
+ - Still 0 mismatches against ffprobe 8.1.1, now on 126 committed fixtures: 114 made with ffmpeg and 12 real MediaRecorder recordings from Chromium ([benchmarks](docs/benchmarks.md#accuracy-vs-ffprobe)).
80
+
81
+ ### Fixed
82
+
83
+ - A worker script that failed to load, such as a 404 after a redeploy, was reported as a crash. It now rejects with `reason: 'load'` and says the app may have been redeployed; when the last worker fails to load, the queued jobs reject once instead of each loading the script again.
84
+ - The mp4box engine ended a fragmented track at the sum of its sample durations. It now follows the tfdt timeline like ffprobe, so a Chrome MediaRecorder fMP4 gets ffprobe's duration (4.524533 s instead of 4.557667 s on `rec_chromium_h264.mp4`).
85
+
86
+ ### Known limitations
87
+
88
+ - React 18 and 19 only. React is an optional peer, `^18.0.0 || ^19.0.0`: next to another React major npm refuses the install with `ERESOLVE`, unless you pass `--legacy-peer-deps` or add `"overrides": { "video-fps-probe": { "react": "$react" } }` to your `package.json`.
89
+ - Zero-config Vite (iife workers) loads about 39 KB for the first MP4, one worker with both parsers; `worker: { format: 'es' }` makes it about 23.6 KB.
90
+ - Only Vite 8 and npm are tested in CI; other bundlers are documented ([bundlers](docs/bundlers.md)), and Yarn, pnpm and bun are expected to work.
91
+ - Browser floors (Chrome and Edge 94, Firefox 114, Safari 15.4, mp4box.js Safari 16.4) are derived, not tested; CI runs Playwright's Chromium, Firefox and WebKit, and in-thread `analyze()` is tested on Node 24 only.
92
+ - The type declarations name DOM types, so a TypeScript project for Node needs `"DOM"` in `lib` or `skipLibCheck: true`.
93
+ - The corpus has real recordings from Chromium's MediaRecorder only: none from Firefox, Safari, phones, cameras or screen recorders.
94
+ - A regular cadence such as 2:3 pulldown is VFR, but its rate profile reads as Steady.
95
+ - `check` judges the stored frame size: the sample aspect ratio of a `pasp` box or a Matroska DisplayWidth is not applied.
96
+ - `check` takes the duration of a VFR WebM or Matroska track on 1 ms ticks from its measured span, up to one frame shorter than ffprobe's: `vfr_screenrec_ts1000.webm` lasts 5.999 s against 6.000000, so `duration: { min: 6 }` fails it.
97
+ - rVFC needs playback: a visible foreground tab, a playable codec and seconds per file.
98
+ - Files above 2^53 bytes, `cmov` headers and a moov above 256 MiB are rejected as error results.
99
+ - In `auto` mode a large Matroska file gets a sampled probe unless the exact scan is predicted to take at most 150 ms.
100
+
101
+ ### Not yet
102
+
103
+ - Bundler matrix and peers job (0.3.0): Vite 6, webpack 5, Rspack, Next.js 16 on Turbopack and webpack, a pnpm install and a Yarn PnP install.
104
+ - Demo: no prewarm before the first file.
105
+
106
+ ## 0.1.0 - 2026-10-24
107
+
108
+ First release; not on npm yet (`"private": true`).
109
+
110
+ ### Added
111
+
112
+ - Frame rate (an exact rational and ffprobe's `avg_frame_rate`), nominal, minimum and maximum FPS, frame counts, duration, codec, size and rotation per video track, a VFR flag and a rate profile, read from MP4, MOV (fragmented included), WebM and Matroska headers in a module worker, without decoding. Values carry provenance tags; file problems are `result.error`.
113
+ - Entries: `.` and `./lazy` with the own worker, `./client` and `./client/lazy` for a host worker, the `./mp4box`, `./mediabunny` and `./rvfc` engines, `./worker-runtime` and `./engines/*` for a custom worker, and `./standalone/own.worker.js` ([API](docs/api.md)).
114
+ - `analyze` with `engine` and `fallback`, `tryAnalyze`, `compare` (experimental), `prewarm`, `dispose`, `destroy`, `summarize`, `ProbeError`, the in-thread `analyze` for Node, and worker protocol v1.
115
+ - Chrome and Edge 94, Firefox 114, Safari 15.4 (derived floors); CI runs Chromium, Firefox and WebKit.
116
+ - 0.67 KB gzip on the critical path of a Vite 8.3.5 host; the first MP4 loads 23.26 KB lazily with `worker: { format: 'es' }`.
117
+ - 0 mismatches against ffprobe 8.1.1 on the 111 committed fixtures.
118
+
119
+ ### Known limitations
120
+
121
+ - Zero-config Vite (iife workers) loads 34.77 KB for the first MP4: the probe client and one 32.11 KB worker file with both parsers.
122
+ - A regular cadence such as 2:3 pulldown is VFR, but its rate profile reads as Steady.
123
+ - rVFC needs playback: a visible foreground tab, a playable codec and seconds per file.
124
+ - Files above 2^53 bytes, `cmov` headers and a moov above 256 MiB are rejected as error results.
125
+ - In `auto` mode a large Matroska file gets a sampled probe unless the exact scan is predicted to take at most 150 ms.
126
+ - Only Vite 8 is verified; the other bundlers are documented, not tested ([bundlers](docs/bundlers.md)).
127
+
128
+ ### Not yet
129
+
130
+ - React entry (0.2.0), bundler matrix and peers job (0.3.0), npm publishing (1.0.0).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Volodymyr Bondarets
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,183 @@
1
- # Temporary Holding Version
1
+ # video-fps-probe
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Frame rate, frame count, VFR detection and a rate profile for MP4, MOV, WebM and MKV files, read from the container headers in a Web Worker. Nothing is decoded and nothing leaves the browser, and the values match ffprobe 8.1.1 on the test corpus (0 mismatches). ESM only, no runtime dependencies, types included.
4
+
5
+ **[Live demo](https://vbondarets.github.io/video-fps-probe/)** · [API](docs/api.md) · [Bundlers](docs/bundlers.md) · [Benchmarks](docs/benchmarks.md) · [Architecture](docs/architecture.md) · [Changelog](CHANGELOG.md)
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm install video-fps-probe
11
+ yarn add video-fps-probe # Yarn classic and berry
12
+ pnpm add video-fps-probe
13
+ bun add video-fps-probe
14
+ ```
15
+
16
+ The first npm release is 0.2.0; one publish serves every package manager ([publishing](docs/publishing.md)). npm is the tested package manager: CI installs the packed tarball with npm. Yarn, pnpm and bun are expected to work but are not tested in CI yet (pnpm, Yarn PnP and bun installs join the bundler matrix in 0.3.x). The package has no dependencies. Its peers are optional and no package manager installs them for you: install one yourself, only for the entry you import: `mp4box` for `video-fps-probe/mp4box`, `mediabunny` for `video-fps-probe/mediabunny`, React 18 or 19 for `video-fps-probe/react`.
17
+
18
+ For a commit that is not released yet, install a packed tarball. A git dependency gets no code with npm (Yarn, pnpm and bun are not tested): `prepack` builds `dist/`, and npm does not run it for a git install.
19
+
20
+ ```
21
+ git clone https://github.com/vbondarets/video-fps-probe.git && cd video-fps-probe
22
+ npm ci && TGZ="$PWD/$(npm pack | tail -n 1)" # builds and checks dist/, writes the tarball, keeps its path
23
+ cd ../your-app && npm install "$TGZ"
24
+ ```
25
+
26
+ In PowerShell: `$tgz = Join-Path $PWD (npm pack | Select-Object -Last 1)`, then `cd ../your-app; npm install $tgz`.
27
+
28
+ ## Quick start
29
+
30
+ ```ts
31
+ import { lazyProbe } from 'video-fps-probe/lazy';
32
+
33
+ const probe = lazyProbe(); // a small stub: loads nothing until the first call
34
+
35
+ const result = await probe.analyze(file); // a broken file is result.error, not a rejection
36
+ const track = result.videoTracks.find((t) => t.trackId === result.primaryTrackId);
37
+ track?.avgFps; // exact rational { num, den, value }
38
+ track?.avgFpsFfprobe; // the avg_frame_rate string of ffprobe, for example '30000/1001'
39
+ track?.presentedFrames; // the frame count
40
+ track?.isVfr;
41
+ track?.rateProfile?.summary; // for example 'Uneven: 5 fps for 4 s, 200 fps for 4 s'
42
+ ```
43
+
44
+ The first call loads the probe client and the own worker of the file's container family, picked by its first 4 bytes, with the parser inside; `probe.prewarm()` loads them ahead, for example when an upload dialog opens. `createProbe()` from `video-fps-probe` is the probe without the stub; `summarize(result)` flattens the primary track.
45
+
46
+ ## React
47
+
48
+ ```tsx
49
+ import { useState } from 'react';
50
+ import { lazyProbe } from 'video-fps-probe/lazy';
51
+ import { VideoFpsProvider, useVideoFps } from 'video-fps-probe/react';
52
+
53
+ const probe = lazyProbe(); // once, at module scope
54
+
55
+ export const App = () => (
56
+ <VideoFpsProvider probe={probe}>
57
+ <Upload />
58
+ </VideoFpsProvider>
59
+ );
60
+
61
+ function Upload() {
62
+ const [file, setFile] = useState<File | null>(null);
63
+ const { status, summary } = useVideoFps(file); // 'idle', 'pending', 'success' or 'error'; also result, error, progress, cancel(), retry()
64
+ return (
65
+ <>
66
+ <input type="file" onChange={(e) => setFile(e.currentTarget.files?.[0] ?? null)} />
67
+ {status === 'success' && <p>{summary?.ok ? `${summary.fps?.toFixed(3)} fps` : summary?.error}</p>}
68
+ </>
69
+ );
70
+ }
71
+ ```
72
+
73
+ React 18 and 19. `useVideoFps` caches each result by the file and the options, runs one analysis through the double effects of `StrictMode`, and aborts a pending one when the last component using it unmounts; `usePrewarmVideoFps()` returns a function that prewarms the probe ([details](docs/api.md#react)).
74
+
75
+ ## Engines
76
+
77
+ Own, the default, needs nothing else. mp4box.js and mediabunny are optional peers for a second opinion or a fallback, and rVFC estimates by playing the file:
78
+
79
+ ```ts
80
+ import { lazyProbe } from 'video-fps-probe/lazy';
81
+ import { mediabunny } from 'video-fps-probe/mediabunny'; // npm install mediabunny
82
+ import { mp4box } from 'video-fps-probe/mp4box'; // npm install mp4box
83
+
84
+ const probe = lazyProbe({ engines: [mp4box(), mediabunny()] });
85
+ await probe.analyze(file, { engine: 'mediabunny' });
86
+ await probe.analyze(file, { fallback: ['mediabunny'] }); // on an error, no frame rate, a timeout or a crash
87
+ const { outcomes, agreement } = await probe.compare(file); // experimental
88
+ ```
89
+
90
+ `agreement.agree` is `true`, `false`, or `null` when nothing could be compared ([rules](docs/api.md#compare-and-agreement)). rVFC (`video-fps-probe/rvfc`) is never used implicitly and needs a visible host element ([engines](docs/api.md#engines)).
91
+
92
+ ## Node
93
+
94
+ ```ts
95
+ import { openAsBlob } from 'node:fs';
96
+ import { analyze } from 'video-fps-probe';
97
+
98
+ const result = await analyze(await openAsBlob(path), { name: path }); // in the calling thread, no worker
99
+ ```
100
+
101
+ In-thread `analyze()` is tested on Node 24, the version in `.nvmrc`, and on Node 22.12, the lowest supported version: CI runs the unit, fixture and package tests on both. The example needs `fs.openAsBlob`, available since Node 19.8 and stable in Node 24. Files above 4 GiB need a `BlobLike` in Node 24 ([in-thread analyze](docs/api.md#in-thread-analyze)). The type declarations name DOM types such as `Worker` and `HTMLElement`, so a TypeScript project for Node needs `"DOM"` in `lib` or `skipLibCheck: true`.
102
+
103
+ CommonJS works on Node 22.12 and newer, which load ES modules from `require()`: `const { analyze } = require('video-fps-probe')`, the subpaths included, and a TypeScript project with `"module": "commonjs"` and `moduleResolution` `node10`, such as a NestJS 11 app. The package test checks both from the packed tarball on Node 22.12 and 24. Jest needs babel-jest to transform the package, with plugins for `import.meta` and `import()`: the tested configuration and a stub for unit tests are in [Jest](docs/api.md#jest).
104
+
105
+ ## Cancellation, timeouts and failing open
106
+
107
+ ```ts
108
+ probe.prewarm(); // when the upload dialog opens
109
+ const deadline = new AbortController();
110
+ const late = new Promise<null>((resolve) => setTimeout(() => (deadline.abort(), resolve(null)), 5000));
111
+ const checks = Promise.all(files.map((f) => probe.tryAnalyze(f, { timeoutMs: 3000, signal: deadline.signal })));
112
+ const results = await Promise.race([checks, late]); // null, or null entries: upload without the check
113
+ ```
114
+
115
+ `tryAnalyze` resolves `null` where `analyze` rejects. `timeoutMs` counts per job once a worker takes it and the signal cancels queued and running jobs, but neither covers main-thread chunk downloads: hence `prewarm()` and the race. A probe and a lazy stub reject only with a `ProbeError`; code that did not load, such as a chunk that is gone after a redeploy, has `reason: 'load'`. [Details](docs/api.md#cancellation-and-failing-open).
116
+
117
+ ## Check before upload
118
+
119
+ ```ts
120
+ import { check } from 'video-fps-probe/check';
121
+ import { klingO1ReferenceVideo } from 'video-fps-probe/presets';
122
+
123
+ const report = check(await probe.tryAnalyze(file, { timeoutMs: 3000 }), klingO1ReferenceVideo.rules);
124
+ if (report.verdict === 'fail') showErrors(report.violations, report.suggestedFps?.reason); // for example 'convert to 24 fps'
125
+ else upload(file); // 'pass', 'warn', or 'unknown': no result, an error or a missing field fails open
126
+ ```
127
+
128
+ Rules are JSON, such as `{ fps: { min: 24, max: 60 }, duration: { max: 10 }, vfr: 'warn' }`, and frame rates compare as exact rationals: 23.976 fps (`24000/1001`) is below a minimum of 24, 59.94 meets a maximum of 60. The presets hold the published input limits of Kling O1 and Motion Control, Runway Aleph 2.0, Veo 3.1, and Wan 2.6 on fal and Seedance 2.0 on EvoLink (gateways, so they only warn), each with its source and the day it was verified; the source pages are also in `video-fps-probe/presets/sources.json`, where `sources[name]` replaces the deprecated `sourceUrl` ([check](docs/api.md#check), [presets](docs/api.md#presets)).
129
+
130
+ ## Size
131
+
132
+ <!-- tables:size:start -->
133
+
134
+ **0.67 KB gzip on the critical path**: the package code in the entry chunk of a Vite 8.3.5 app that imports `video-fps-probe/lazy` (1.40 KB with Vite's preload helper). The rest loads lazily, on the first file:
135
+
136
+ | Host Vite config | Critical path | First MP4 | First MKV | All own paths | Worker files |
137
+ |---|---:|---:|---:|---:|---:|
138
+ | `worker: { format: 'es' }` (recommended) | **0.67 KB** | 22.73 KB | 25.54 KB | 48.61 KB | 2 |
139
+ | none (Vite's default, 'iife') | **0.66 KB** | 24.47 KB | 27.08 KB | 48.27 KB | 2 |
140
+
141
+ Own runs on one worker per container family, picked by the first four bytes of the file, so a first file loads only the worker of its family. An iife worker cannot split, so without the option each own worker is one file with its lazy parts inlined:
142
+
143
+ ```js
144
+ import { defineConfig } from 'vite';
145
+
146
+ export default defineConfig({ worker: { format: 'es' } });
147
+ ```
148
+
149
+ Engines cost nothing until imported. Importing a descriptor grows the critical path a little, from 0.67 KB to 0.89 KB with mp4box or mediabunny, 0.77 KB with rVFC and 1.01 KB with all three, and the engine itself loads lazily: mp4box 44.92 KB, mediabunny 54.15 KB, rVFC 2.19 KB. Gzip -9 of production builds from the packed tarball, budgeted in CI; every configuration is in [docs/benchmarks.md](docs/benchmarks.md#package-size).
150
+
151
+ `video-fps-probe/react` adds 1.20 KB to the critical path for the provider, `useVideoFps` and its store (React itself not counted); its first analysis also fetches a 0.43 KB summary module, in parallel with the probe client. `video-fps-probe/check` adds 3.99 KB when imported statically, and nothing lazily; one preset from `video-fps-probe/presets` adds 0.46 KB.
152
+
153
+ <!-- tables:size:end -->
154
+
155
+ ## Accuracy
156
+
157
+ <!-- tables:accuracy-summary:start -->
158
+
159
+ **0 mismatches against ffprobe 8.1.1** on the 130 committed fixtures: 2054 field checks, 1976 exact, 13 within tolerance (9 of them a definition difference by design: the measured average of a Matroska file that stores no duration for its last frame, which avgFps gives the median interval and ffprobe its own estimate), 0 known deviations with a recorded reason, 65 not applicable. The same comparison runs mp4box.js 2.4.1 and mediabunny 1.62.1 in 5 arms (their idiomatic recipes, mediabunny with a full scan, and this package's tuned adapters) and sorts each difference into wrong, definition differs, unverified, not provided or unsupported container; the [accuracy section](docs/benchmarks.md#accuracy-vs-ffprobe) has the outcomes and where the libraries win.
160
+
161
+ Of the 130 committed fixtures, 118 are synthetic files made with ffmpeg and 12 are real MediaRecorder recordings from headless Chromium, committed as recorded; per-engine tables, speed and method: [docs/benchmarks.md](docs/benchmarks.md).
162
+
163
+ <!-- tables:accuracy-summary:end -->
164
+
165
+ ## Browser support
166
+
167
+ Chrome and Edge 94, Firefox 114, Safari and iOS 15.4: floors derived from the features used, not tested on those versions; CI runs Chromium, Firefox and WebKit. mp4box.js needs Safari 16.4; rVFC needs `requestVideoFrameCallback` (Chrome 83, Firefox 132, Safari 15.4), a visible foreground tab and a playable codec. [Details](docs/benchmarks.md#browser-support).
168
+
169
+ Known limitations: [changelog](CHANGELOG.md#known-limitations), [benchmarks](docs/benchmarks.md#limitations). Development: [demo/README.md](demo/README.md), [scripts](docs/benchmarks.md#reproducing-the-numbers).
170
+
171
+ ## Roadmap
172
+
173
+ - **0.3.x**: a bundler matrix in CI: webpack 5, Rspack, Next.js, Angular CLI, SvelteKit, Nuxt, Parcel, Bun and Deno, with pnpm, Yarn PnP and bun installs.
174
+ - **0.4.0**: a header-only ISO BMFF mode with byte, read and time limits, machine-readable error codes and a container layout field, a Range source for S3 and R2 style object storage (a fetcher function, no SDK dependency), minimal color, HDR and Dolby Vision metadata, the pixel aspect ratio and display size, a median frame interval that `check` uses, and a CommonJS build of the server entries only, while the browser entries stay ESM.
175
+ - **0.5.0**: a Node entry (`fromPath`, DOM-free types, a `worker_threads` pool), NestJS validators, a CLI, bitrate, GOP and audio details, cadence detection, framework adapters for Vue, Svelte and Angular, and later MPEG-TS and HLS.
176
+
177
+ ## Feedback
178
+
179
+ Questions, and how you use the package, go to [GitHub Discussions](https://github.com/vbondarets/video-fps-probe/discussions). For a wrong value or a setup that does not build, open a [bug report](https://github.com/vbondarets/video-fps-probe/issues/new/choose) with the browser or Node version, the container, the Copy JSON output of the demo or the result JSON, and the ffprobe output if you have it. The package and the demo collect no telemetry.
180
+
181
+ ## License
182
+
183
+ [MIT](LICENSE). The optional peers have their own licenses: mediabunny is MPL-2.0, mp4box is BSD-3-Clause.
@@ -0,0 +1,9 @@
1
+ import type { ProbeResult } from './client/types.js';
2
+ import type { BlobLike } from './core/reader.js';
3
+ import type { AnalyzeOptions } from './core/types.js';
4
+ import type { EngineDescriptor } from './plugins/types.js';
5
+ export interface InThreadOptions extends AnalyzeOptions {
6
+ name?: string;
7
+ engine?: EngineDescriptor;
8
+ }
9
+ export declare function analyze(input: Blob | BlobLike, options?: InThreadOptions): Promise<ProbeResult>;
@@ -0,0 +1,5 @@
1
+ export async function analyze(input, options = {}) {
2
+ const { name = typeof File !== 'undefined' && input instanceof File ? input.name : '', engine, ...opts } = options;
3
+ const module = engine ? await engine.load() : (await import("./worker/runtime.js")).ownEngine;
4
+ return module.run(input, name, opts);
5
+ }
@@ -0,0 +1,18 @@
1
+ import type { EngineId, FileResult, Rational } from '../core/types.js';
2
+ export type ComparedFieldName = 'avgFps' | 'nominalFps' | 'minFps' | 'maxFps' | 'frames' | 'durationSec';
3
+ export type ComparedValue = Rational | number | null;
4
+ export interface ComparedField<K extends EngineId> {
5
+ field: ComparedFieldName;
6
+ values: Partial<Record<K, ComparedValue>>;
7
+ agree: boolean | null;
8
+ differ: K[];
9
+ }
10
+ export interface Agreement<K extends EngineId> {
11
+ reference: K | null;
12
+ engines: K[];
13
+ fields: ComparedField<K>[];
14
+ agree: boolean | null;
15
+ }
16
+ export declare const RELATIVE_TOLERANCE = 0.003;
17
+ export declare const DURATION_TOLERANCE_SEC = 0.000001;
18
+ export declare function compareResults<K extends EngineId>(results: Partial<Record<K, FileResult>>, reference?: NoInfer<K>): Agreement<K>;
@@ -0,0 +1,55 @@
1
+ import { primaryTrack } from "../core/result.js";
2
+ export const RELATIVE_TOLERANCE = 0.003;
3
+ export const DURATION_TOLERANCE_SEC = 1e-6;
4
+ const RATIONALS = ['avgFps', 'nominalFps', 'minFps', 'maxFps'];
5
+ const APPROXIMATE = new Set(['estimated', 'sampled']);
6
+ const sameRational = (a, b) => [a.num, a.den, b.num, b.den].every(Number.isSafeInteger) ? BigInt(a.num) * BigInt(b.den) === BigInt(b.num) * BigInt(a.den) : a.num / a.den === b.num / b.den;
7
+ function valueOf(t, field) {
8
+ if (field === 'frames')
9
+ return t.presentedFrames ?? t.frames;
10
+ if (field === 'durationSec')
11
+ return t.durationSec;
12
+ return t[field];
13
+ }
14
+ function equal(field, a, b) {
15
+ const x = valueOf(a, field);
16
+ const y = valueOf(b, field);
17
+ if (typeof x === 'number' && typeof y === 'number')
18
+ return field === 'durationSec' ? Math.abs(x - y) <= DURATION_TOLERANCE_SEC : x === y;
19
+ const p = x;
20
+ const q = y;
21
+ if (sameRational(p, q))
22
+ return true;
23
+ const approximate = APPROXIMATE.has(a.provenance[field]) || APPROXIMATE.has(b.provenance[field]);
24
+ return approximate && Math.abs(p.value / q.value - 1) <= RELATIVE_TOLERANCE;
25
+ }
26
+ export function compareResults(results, reference) {
27
+ const tracks = new Map();
28
+ for (const [engine, result] of Object.entries(results)) {
29
+ const t = result && !result.error ? primaryTrack(result) : null;
30
+ if (t)
31
+ tracks.set(engine, t);
32
+ }
33
+ const engines = [...tracks.keys()];
34
+ const ref = reference !== undefined ? (tracks.has(reference) ? reference : null) : tracks.has('own') ? 'own' : (engines[0] ?? null);
35
+ const fields = [...RATIONALS, 'frames', 'durationSec'].map((field) => {
36
+ const values = {};
37
+ for (const [engine, t] of tracks)
38
+ values[engine] = valueOf(t, field);
39
+ const base = ref === null ? null : tracks.get(ref);
40
+ const differ = [];
41
+ let compared = 0;
42
+ if (base && valueOf(base, field) !== null) {
43
+ for (const [engine, t] of tracks) {
44
+ if (engine === ref || valueOf(t, field) === null)
45
+ continue;
46
+ compared++;
47
+ if (!equal(field, base, t))
48
+ differ.push(engine);
49
+ }
50
+ }
51
+ return { field, values, agree: compared === 0 ? null : differ.length === 0, differ };
52
+ });
53
+ const decided = fields.filter((f) => f.agree !== null);
54
+ return { reference: ref, engines, fields, agree: decided.length === 0 ? null : decided.every((f) => f.agree) };
55
+ }
@@ -0,0 +1,12 @@
1
+ import type { EngineId } from '../core/types.js';
2
+ export type ProbeErrorCode = 'aborted' | 'timeout' | 'worker-failed' | 'engine-not-registered' | 'unknown-engine' | 'unsupported-environment' | 'protocol';
3
+ export interface ProbeErrorOptions {
4
+ engine?: EngineId | null;
5
+ cause?: unknown;
6
+ }
7
+ export declare class ProbeError extends Error {
8
+ readonly code: ProbeErrorCode;
9
+ readonly engine: EngineId | null;
10
+ readonly reason?: 'load' | 'crash';
11
+ constructor(code: ProbeErrorCode, message: string, options?: ProbeErrorOptions);
12
+ }
@@ -0,0 +1,10 @@
1
+ export class ProbeError extends Error {
2
+ code;
3
+ engine;
4
+ constructor(code, message, options = {}) {
5
+ super(message, options.cause === undefined ? undefined : options);
6
+ this.name = 'ProbeError';
7
+ this.code = code;
8
+ this.engine = options.engine ?? null;
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ import type { ProbeError } from './errors.js';
2
+ export declare const isProbeError: (e: unknown) => e is ProbeError;
3
+ export declare function isAbortError(e: unknown): boolean;
4
+ export declare function isTimeoutError(e: unknown): boolean;
@@ -0,0 +1,9 @@
1
+ const field = (e, key) => (typeof e === 'object' && e !== null && key in e ? e[key] : undefined);
2
+ export const isProbeError = (e) => field(e, 'name') === 'ProbeError' && typeof field(e, 'code') === 'string';
3
+ const probeCode = (e, code, otherName) => (isProbeError(e) ? e.code === code : field(e, 'name') === otherName);
4
+ export function isAbortError(e) {
5
+ return probeCode(e, 'aborted', 'AbortError');
6
+ }
7
+ export function isTimeoutError(e) {
8
+ return probeCode(e, 'timeout', 'TimeoutError');
9
+ }
@@ -0,0 +1,22 @@
1
+ import type { ContainerKind, EngineId, FileResult } from '../core/types.js';
2
+ export interface VideoFpsSummary {
3
+ ok: boolean;
4
+ name: string;
5
+ container: ContainerKind;
6
+ engine: EngineId;
7
+ fps: number | null;
8
+ fpsRational: string | null;
9
+ nominalFps: number | null;
10
+ frames: number | null;
11
+ durationSec: number | null;
12
+ width: number | null;
13
+ height: number | null;
14
+ rotation: number | null;
15
+ isVfr: boolean | null;
16
+ minFps: number | null;
17
+ maxFps: number | null;
18
+ approximate: boolean;
19
+ error: string | null;
20
+ warnings: string[];
21
+ }
22
+ export declare function summarize(r: FileResult): VideoFpsSummary;
@@ -0,0 +1,25 @@
1
+ const APPROXIMATE = new Set(['estimated', 'sampled']);
2
+ export function summarize(r) {
3
+ const t = r.primaryTrackId === null ? null : (r.videoTracks.find((x) => x.trackId === r.primaryTrackId) ?? null);
4
+ const error = r.error ?? (!t ? 'no video track' : t.avgFps ? null : 'no frame rate');
5
+ return {
6
+ ok: error === null,
7
+ name: r.name,
8
+ container: r.container,
9
+ engine: r.engine,
10
+ fps: t?.avgFps?.value ?? null,
11
+ fpsRational: t?.avgFps ? `${t.avgFps.num}/${t.avgFps.den}` : null,
12
+ nominalFps: t?.nominalFps?.value ?? null,
13
+ frames: t ? (t.presentedFrames ?? t.frames) : null,
14
+ durationSec: t?.durationSec ?? r.formatDurationSec,
15
+ width: t?.width ?? null,
16
+ height: t?.height ?? null,
17
+ rotation: t?.rotation ?? null,
18
+ isVfr: t?.isVfr ?? null,
19
+ minFps: t?.minFps?.value ?? null,
20
+ maxFps: t?.maxFps?.value ?? null,
21
+ approximate: !!t && ['avgFps', 'nominalFps', 'frames'].some((k) => APPROXIMATE.has(t.provenance[k])),
22
+ error,
23
+ warnings: [...r.warnings, ...(t?.warnings ?? [])],
24
+ };
25
+ }
@@ -0,0 +1,60 @@
1
+ import type { ContainerKind, FileResult } from '../core/types.js';
2
+ export type RatioValue = number | `${number}/${number}` | `${number}:${number}`;
3
+ export type RatePolicy = 'allow' | 'warn' | 'reject';
4
+ export type FpsSource = 'avg' | 'ffprobe' | 'nominal' | 'dominant';
5
+ export type CheckVerdict = 'pass' | 'warn' | 'fail' | 'unknown';
6
+ export type CheckSeverity = 'fail' | 'warn' | 'unknown';
7
+ export interface RangeRule<T = number> {
8
+ min?: T;
9
+ max?: T;
10
+ }
11
+ export interface FpsRule extends RangeRule<RatioValue> {
12
+ exact?: RatioValue;
13
+ oneOf?: readonly RatioValue[];
14
+ source?: FpsSource;
15
+ }
16
+ export interface AspectRatioRule extends RangeRule<RatioValue> {
17
+ oneOf?: readonly RatioValue[];
18
+ }
19
+ export interface FrameSizeRule {
20
+ minWidth?: number;
21
+ maxWidth?: number;
22
+ minHeight?: number;
23
+ maxHeight?: number;
24
+ minShortSide?: number;
25
+ maxShortSide?: number;
26
+ minLongSide?: number;
27
+ maxLongSide?: number;
28
+ }
29
+ export interface CheckRules {
30
+ fps?: FpsRule;
31
+ duration?: RangeRule;
32
+ frameSize?: FrameSizeRule;
33
+ aspectRatio?: AspectRatioRule;
34
+ pixels?: RangeRule;
35
+ fileSize?: RangeRule;
36
+ containers?: readonly ContainerKind[];
37
+ codecs?: readonly string[];
38
+ audio?: 'required' | 'forbidden';
39
+ vfr?: RatePolicy;
40
+ uneven?: RatePolicy;
41
+ severity?: 'fail' | 'warn';
42
+ }
43
+ export type CheckExpected = number | string | readonly (number | string)[] | null;
44
+ export interface CheckViolation {
45
+ rule: string;
46
+ severity: CheckSeverity;
47
+ expected: CheckExpected;
48
+ actual: number | string | boolean | null;
49
+ message: string;
50
+ }
51
+ export interface SuggestedFps {
52
+ value: number | null;
53
+ reason: string;
54
+ }
55
+ export interface CheckReport {
56
+ verdict: CheckVerdict;
57
+ violations: CheckViolation[];
58
+ suggestedFps?: SuggestedFps;
59
+ }
60
+ export declare function check(result: FileResult | null | undefined, rules: CheckRules): CheckReport;