Skip to content

Repository files navigation

Video FPS Calculator

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.


Contents


Download (no setup required)

The standalone Windows executable is committed at the root of this repository:

  1. Open VideoFPSCalculator.exe.
  2. Click Download raw file.
  3. 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.


Example report

Example Video FPS Report

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

Graphical app

python fps_calculator_gui.py

Or 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.


Command line

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" --all

The 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

How the frame count is calculated

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.


Duplicate handling

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

Supported formats

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.


The HTML report

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.


Running from source

Requirements

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.

Scripts

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.


Testing

python run_tests.py
Running 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 -v

The 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.


Building the executable

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 --noconfirm

The executable appears in dist/.

Note: a standalone ffprobe.exe is not committed to this repository — download it from https://ffmpeg.org/download.html when building. A copy is, however, embedded inside the pre-built VideoFPSCalculator.exe; see licensing below.


License

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages