A Windows tool that reads the frame rate and duration of video files (via
FFmpeg's ffprobe), calculates the total number of
frames as FPS × duration, and produces a clean, shareable HTML report.
It works on a single file or recursively across an entire folder, detects duplicate videos by content hash so identical files are only counted once, and comes in two flavours: a compact graphical app and a command-line script.
- Download
- Example report
- Graphical app
- Command line
- How the frame count is calculated
- Duplicate handling
- Supported formats
- The HTML report
- Running from source
- Testing
- Building the executable
- License
The standalone Windows executable is committed at the root of this repository:
- Open
VideoFPSCalculator.exe. - Click Download raw file.
- Double-click it — that's it.
ffprobe is bundled inside the executable, so nothing else needs installing.
Note that the bundled ffprobe is GPL-licensed — see
licensing and THIRD-PARTY-NOTICES.md.
Windows SmartScreen may warn on first run because the executable is unsigned. The build is reproducible from source with the included
VideoFPSCalculator.spec.
The full markup is in sample_report.html. The example
above shows all of the report's behaviour at once: five unique videos, one
byte-identical duplicate excluded from the totals, and one damaged file
surfaced as an error.
| # | Filename | FPS | Duration | Total Frames | Info |
|---|---|---|---|---|---|
| 1 | clip_30fps.mp4 | 30.0 | 0:03 | 90 | 1920x1080 H264 |
| 2 | clip_60fps.mp4 | 60.0 | 0:02 | 120 | 1920x1080 H264 |
| 3 | clip_ntsc_2997.mp4 | 29.97 | 0:04 | 120 | 1920x1080 H264 |
| 4 | clip_ntsc_2997_copy.mp4 | 29.97 | 0:04 | (duplicate — not counted) | 1920x1080 H264 |
| 5 | clip_pal_25.mkv | 25.0 | 0:02 | 50 | 1280x720 H264 |
| 6 | damaged.mp4 | — | — | Damaged or incomplete mov file: moov atom not found |
python fps_calculator_gui.pyOr run VideoFPSCalculator.exe.
- Pick a file or a folder (folders are searched recursively).
- Choose where to save the report.
- Tick Any file type to probe every file regardless of extension — useful for footage with an unusual or missing extension.
- Tick Auto-open to open the report as soon as it is written.
- Click Analyze.
The status line reports progress while scanning, hashing and writing, then summarises the result: files found, unique videos, duplicates and total frames.
python fps_calculator.py <video_file_or_folder> [output.html] [--all]| Argument | Meaning |
|---|---|
<video_file_or_folder> |
File to analyse, or folder to search recursively |
[output.html] |
Report path (default fps_report.html) |
--all, -a |
Probe every file regardless of extension |
Examples:
# One file, default report name
python fps_calculator.py "C:\Videos\clip.mp4"
# A whole folder, custom report name
python fps_calculator.py "C:\Videos" "report.html"
# Probe every file, whatever its extension
python fps_calculator.py "C:\Videos" "report.html" --allThe console output ends with the same summary the report shows:
Video files found : 6
Unique video files : 5
Duplicate files : 1
Total frames (unique) : 380
Errors : 1
total frames = round(FPS × duration)
The subtlety is which frame rate to use. ffprobe reports two, and they are
not interchangeable:
| Field | What it actually is |
|---|---|
avg_frame_rate |
Total frames ÷ duration — what players and Windows Properties show |
r_frame_rate |
The base rate: the lowest rate whose grid can express every timestamp exactly |
r_frame_rate is not the frame rate. For MPEG transport streams
(time_base 1/90000), raw elementary streams and variable-frame-rate content it
inflates to a multiple of the timestamp clock — in the worst case exactly
90000/1, the 90 kHz MPEG clock. Trusting it reports a 14.91 fps capture as
90000 fps and inflates the frame count by six orders of magnitude.
This tool therefore prefers avg_frame_rate, falls back to r_frame_rate (needed
for raw streams, which often report avg_frame_rate as 0/0), and finally
derives the rate from nb_frames ÷ duration. Candidates that match the
container's timestamp clock, or exceed a plausibility ceiling, are rejected
rather than reported. Where no sane rate can be determined the file is reported
as an error instead of guessing.
Because avg_frame_rate is by definition frames ÷ duration, FPS × duration
lands on the true frame count for variable-frame-rate video too.
Unknown duration. Raw elementary streams (.h264, .hevc, …) carry no
container timing, so duration is unknown and the frame count is unknowable.
Those files show ? and N/A rather than 0, and the report states how many
were excluded from the total so the shortfall is never silent.
The same video often exists several times under different names. Counting it repeatedly inflates every total, so duplicates are detected and excluded.
- Uniqueness is byte-for-byte, established with a SHA-256 hash of file contents. Filenames, timestamps and locations are irrelevant.
- Only unique videos count toward Total Frames. Duplicates still appear in
the report — shaded, and labelled
duplicate of <name>— so nothing is hidden. - When several copies exist, the shallowest path is kept as the canonical one, ties broken by scan order.
Hashing is cheap in practice: byte-identical files must have identical sizes, so only files that collide on size are ever read. A file whose size is unique in the set is settled without being opened.
So for four files where three are copies of one video:
Video files found : 4
Unique video files : 2
Duplicate files : 2
68 extensions, covering essentially anything ffprobe can demux:
| Family | Extensions |
|---|---|
| MP4 / QuickTime | .mp4 .m4v .mov .qt .mqv .3gp .3g2 .3gpp .3gp2 |
| Matroska / WebM | .mkv .mk3d .webm |
| AVI / Windows | .avi .divx .wmv .asf .wtv .dvr-ms .amv |
| MPEG program & transport streams (DVD, Blu-ray, camcorders, broadcast) |
.mpg .mpeg .mpe .mpv .m1v .m2v .m2p .vob .evo .ts .m2t .m2ts .mts .tts .trp .tp .mod .tod .vro .rec |
| Flash | .flv .f4v .f4p .swf |
| Ogg / Theora | .ogv .ogm .ogx |
| RealMedia | .rm .rmvb |
| Professional / broadcast | .mxf .lxf .gxf .dv .dif |
| Raw elementary streams | .h264 .264 .h265 .265 .hevc .ivf .y4m |
| Other containers | .nsv .nuv .drc .bik .smk .roq .fli .flc |
Anything outside this list can still be analysed with --all / Any file
type, which probes every file and lets ffprobe decide. In that mode a
skip-list of 82 known non-video extensions (images, audio, documents, archives)
avoids wasting a probe on files that cannot be video.
Not everything with a video extension is a video. .ts is shared with
TypeScript, .mpv with mpv config files. Files that no demuxer recognises are
skipped and counted separately — never reported as errors:
Skipped 1 file(s) not recognised as media.
A file whose container is recognised but cannot be read is a different matter
— that is a genuinely damaged video and is reported as an error with the reason
(Damaged or incomplete mov file: moov atom not found).
Audio files carrying embedded cover art are also correctly ignored: that artwork is a single-frame video stream, and counting it would report an album as a video.
A single self-contained HTML file — no external assets, safe to archive or email.
Summary cards: video files found · unique videos · duplicate files · total frames · errors.
Per-file table: index, filename (full path on hover), FPS, duration, total frames, resolution and codec. Duplicate rows are shaded and annotated; error rows are red and carry the reason.
All filenames, paths and error text are HTML-escaped, so unusual characters cannot corrupt or inject into the markup.
- Python 3.10+
- FFmpeg, with
ffprobeon yourPATH(https://ffmpeg.org/download.html)
No pip packages are needed — the tool uses only the standard library, and the
GUI uses Tkinter, which ships with the standard Python installer on Windows.
If ffprobe.exe sits next to fps_calculator.py it is used in preference to
PATH, which is how the bundled executable works.
| File | Purpose |
|---|---|
fps_calculator.py |
Core engine and CLI |
fps_calculator_gui.py |
Tkinter GUI (the entry point for the .exe) |
video_fps_report.py |
Alternative report generator: dark theme, sortable and filterable table, FPS distribution cards, optional OpenCV fallback |
video_fps_report.py shares the frame-rate logic, extension lists and duplicate
detection with fps_calculator.py, so both agree on FPS and frame counts.
python run_tests.pyRunning test suite (one process per module)
test_fps_calculator 122 tests OK
test_video_fps_report 64 tests OK
test_fps_calculator_gui 11 tests OK
TOTAL 197 tests OK
Each module runs in its own process. That is not cosmetic: Tkinter cannot reliably create and tear down several Tk roots in one interpreter while worker threads are alive, and the GUI tests do both.
Individual modules run under unittest or pytest as usual:
python -m unittest test_fps_calculator -v
python -m pytest test_fps_calculator.py -vThe suite covers frame-rate selection (including the 90000 fps timebase case),
duplicate detection, report generation and escaping, non-ASCII paths, damaged
versus non-media files, and GUI error recovery. Tests that need a real ffprobe
synthesise their own media — a hand-built YUV4MPEG2 file for the valid case, a
28-byte ftyp-only MP4 for the damaged case — so no external encoder is
required. They skip automatically if ffprobe is unavailable, as do the GUI
tests without a desktop session.
Built with PyInstaller using the included
VideoFPSCalculator.spec. Place a copy of
ffprobe.exe next to the spec file, then:
pip install pyinstaller
pyinstaller VideoFPSCalculator.spec --noconfirmThe executable appears in dist/.
Note: a standalone
ffprobe.exeis not committed to this repository — download it from https://ffmpeg.org/download.html when building. A copy is, however, embedded inside the pre-builtVideoFPSCalculator.exe; see licensing below.
The source in this repository — fps_calculator.py, fps_calculator_gui.py,
video_fps_report.py, the tests and the build spec — is released under the
MIT License. It contains no FFmpeg code and calls ffprobe as a
separate process.
The pre-built VideoFPSCalculator.exe additionally bundles ffprobe, which
is part of FFmpeg. The bundled build is configured with --enable-gpl --enable-version3, so that binary is GPL-3.0-or-later. Redistributing the
executable therefore carries GPL obligations for the bundled ffprobe,
including making its corresponding source available.
See THIRD-PARTY-NOTICES.md for the exact version, its licence, and where to obtain the corresponding source. Building the executable yourself against an LGPL FFmpeg build avoids the GPL terms.
