System designTroubleshooting

Video FPS returns zero in OpenCV: read frame rate reliably

Why OpenCV returns an FPS of zero on a video file, how to measure the true frame rate by counting frames over time, and when rebuilding the SDK fixes it.

LumaSpec Vision11 min read Share on LinkedIn

On this page 9 sections
  1. 01Why does CAP_PROP_FPS return zero on a file that plays fine?
  2. 02Which cause is it: the file, the build, or the open call?
  3. 03How do you measure the frame rate by counting frames?
  4. 04Why does the counted rate come out lower than the file's rate?
  5. 05How do you record a known-good test clip?
  6. 06When is rebuilding the OpenCV SDK from source the answer?
  7. 07What sanity checks catch a bad FPS value?
  8. 08What is the fix for each case?
  9. 09Questions

You call get(Videoio.CAP_PROP_FPS) on a clip that plays in any player, and the log prints 0. This guide is for you if you extract frames on Android, on a vision PC, or inside an inspection pipeline and need a frame rate you can act on. The zero almost never means your code is wrong: the property reads one metadata field from the container header, and plenty of files that play fine do not carry it. Below you separate the three real causes — the file, the build, the failed open() — measure a rate by counting frames over a timed interval when the header has nothing to say, and decide when rebuilding the SDK from source is worth the effort.

Why does CAP_PROP_FPS return zero on a file that plays fine?

Because get(Videoio.CAP_PROP_FPS) reads one metadata field from the container header, not the frames: zero means the field is missing or your OpenCV build cannot parse it, and your code is correct.

The call chain you wrote is the right one: open the path, then get(Videoio.CAP_PROP_FPS). That property is a single number written into the container at record time, and it is the only place the library looks. Players do not depend on it — they schedule each frame from its own timestamp — so a file with an absent or empty rate field plays everywhere and still answers 0.

Phone pipelines produce most of these files. Messaging apps and screen recorders re-encode on the device and often write variable-frame-rate output, where frames land at uneven intervals and no single rate exists to store; the writer leaves the field out. Zero is the library saying it has no answer, not an error it raised. Clips exported from smart cameras land in the same spot whenever the exporter skips the header.

Print the raw double before any cast: the property is a double, and an early int cast hides fractional rates. When open() itself fails, the symptom looks identical from the FPS property alone — video capture backend fallback: diagnosing AVI open errors walks that half of the problem.

Which cause is it: the file, the build, or the open call?

Test in this order: a failed open() call first, because the check takes ten seconds; missing file metadata second, because it is the most common cause; an OpenCV build that predates the parsing fix third.

  1. Check isOpened(). If open() returned false, no property read means anything. On Android, scoped storage blocks your app from another app's folder under /storage/emulated/0/, so copy the clip into your app storage or fetch it through the media store, then reopen. FPS 0 with isOpened() false is an access problem, not a metadata problem.
  2. Play the known-good clip. Record a fixed-rate clip yourself — the how is two sections down — and read its property. Sane value there, zero on the problem clip: the file lacks the field, and the decision is to measure by counting. Stop debugging the code.
  3. Read every clip you have. Zero on all of them, known-good included, means the build predates the parsing support, and the decision tree ends at the SDK, not at the file.

One report from 2022 matches the third case exactly: the parsing fix had landed in the source tree four days before the user hit it, and every packaged build of the moment predated the fix.

How do you measure the frame rate by counting frames?

Count a fixed block of frames and divide by the elapsed seconds on a monotonic clock: 120 frames read in 4.02 s is 29.9 fps, a number that survives even when the header is empty.

measured fps = frames read / elapsed seconds
  1. Open the clip and confirm isOpened().
  2. Warm up: grab() ten frames and discard them — the first decodes pay the decoder's setup cost.
  3. Read the clock, then pull exactly 120 frames.
  4. Read the clock again and divide: 120 / 4.02 s = 29.9 fps.
VideoCapture cap = new VideoCapture(filePath);
if (!cap.isOpened()) return;               // path or permission
for (int i = 0; i < 10; i++) cap.grab();   // warm-up
long t0 = nowMs();                         // monotonic clock
int n = 0;
while (n < 120 && cap.grab()) n++;
double fps = n / ((nowMs() - t0) / 1000.0);

For the rate the file was written at, divide the container's totals: frame count over duration.

file fps = CAP_PROP_FRAME_COUNT / (CAP_PROP_POS_MSEC at end / 1000)

A 10 s recording at a locked 30 fps holds 300 frames: 300 / 10.0 s = 30.0 fps. Cross-check the two methods — 29.9 counted against 30.0 from totals is a 0.4 percent gap, and agreement within about 2 percent means both numbers are sound. One caution from the field: CAP_PROP_POS_MSEC has not always been milliseconds, with 4.5.5-era builds returning seconds on a first read, so confirm the unit against a clip of known length before dividing.

Time from a monotonic clock — the elapsed-realtime clock on Android — never one the user can set. If the timing loop lives on the machine controller instead of the vision app, the same failure exists there: a GSV wall-clock read returning zero in a CompactLogix breaks the interval math exactly the same way.

Why does the counted rate come out lower than the file's rate?

Because counting measures your decode loop, not the recording: 120 frames that take 6.5 s report 18.5 fps while the file itself is 30 fps — the decoder, not the header, set that number.

The counted value is a floor: the fastest your loop can pull frames on that device. Three things raise it. Use grab() instead of read() when you do not need the pixels, because conversion and copy are the real cost. Measure on the machine that will run the pipeline — a desktop CPU decodes the same clip faster than a phone. And lengthen the run: a 300-frame count averages out the I-frame bursts that a 30-frame count magnifies.

Then compare the floor with the demand. A station that needs 25 fps and a loop that sustains 29.9 fps has 4.9 fps of margin — 20 percent — and passes. A loop that sustains 24 fps fails the same station, and the fix belongs in decode, never in the header. The demand side of the comparison can sit on the controller: troubleshooting a 500 fps frame-rate target against a 4 ms cycle in CODESYS comes down to the same division.

For live cameras the counting logic is identical, only the source changes: how to measure camera frame rate reliably covers the camera side, and camera frame rate measurement from timestamped frames covers the timestamp method. If the frames feed a reader rather than a measurement, decoding barcodes from a live RTSP or MJPEG video stream shows the same grab-loop pattern.

How do you record a known-good test clip?

Lock the camera to a fixed rate, record a round number of seconds, and write MP4 at a constant frame rate: 10 s at 30 fps is 300 frames, and the header then says 30.

Phone cameras change rate with the light, so record the reference clip from a camera whose rate you set. The LS-CMR-0166 is a 12 MP USB3 area scan camera that runs at 30 fps on a 4000 × 3000 sensor. The MV-CS200-10UM is a 20 MP USB3.0 camera on the IMX183 sensor. Fix the rate, fix the exposure so nothing throttles the stream, and record through any writer that sets a constant frame rate; ten seconds at 30 fps gives a file whose truth you know to the frame.

Two area scan cameras that hold a fixed rate while you record:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part
Area scan camerasHikrobot MV-CS200-10UM 20 MP Area Scan Camera, USB3.0, IMX183, 2nd Gen., MonoMV-CS200-10UM
Sensor format
1"
Lens mount
C-mount
$759.00View part

More candidates are in the camera finder.

When is rebuilding the OpenCV SDK from source the answer?

When get(CAP_PROP_FPS) returns zero on every clip including your known-good one: the parsing fix exists in the project's current source tree but not in your prebuilt package, so the build is the thing to change.

Prebuilt Android packages trail the source tree, and fixes for container parsing reach the packages only at the next release. Try the quickest moves first. Update to the newest package, because most fixed builds ship that way. If the newest package still reads zero on every clip, build the Android SDK from the project's current source, taking the source from the official repository rather than third-party re-hosts. Keep the third path open as well: counting frames needs no rebuild and answers today.

The rebuild earns its cost when metadata must be read at scale — batch conversions, archives of thousands of clips, pipelines that branch on the header value. For one number on one file, counting is the decision.

What sanity checks catch a bad FPS value?

Three checks catch almost every bad value: reject 0, reject anything above 240 fps from a handheld clip, and accept 29.97 and 23.98 as real fractional rates instead of rounding them.

Value you read What it means What you do
0 Field missing, build cannot parse it, or the file never opened isOpened() first, known-good clip second, then count frames
29.97 or 23.98 Fractional rates from broadcast timing — real, not noise Keep the double; round only at display
90000 or a similar huge number Timescale returned without its tick division; reported between the 4.8.0.76 and 4.7.0.72 packages, where the older one was right Treat as invalid, change package version, count meanwhile
Plausible but wrong (60 on a 30 fps clip) Header disagrees with the frames Trust the frames: count, or divide frame count by duration

The 240 fps ceiling is a consumer-file bound: phone slow-motion tops out near it, while industrial high-speed cameras exceed it, so apply the ceiling to handheld clips only. Log the OpenCV version beside every FPS you log — most of these failures are version-bound, and the pairing turns a mystery into a one-line answer. The same trust-but-check rule governs camera metadata: per-frame exposure and gain: verifying camera metadata applies it to exposure and gain.

What is the fix for each case?

One line per case: failed open — fix path and permissions; zero on one clip only — count frames; zero on every clip — update, then rebuild from source; nonsense value — change package version.

  • [ ] isOpened() printed before any get() — a failed open masquerades as every other symptom.
  • [ ] A known-good clip in the test set — it separates the file from the build in one run.
  • [ ] Counts of 120 frames or more on a monotonic clock — shorter runs swing with decode bursts.
  • [ ] FPS logged beside the OpenCV version — bad values are usually version-bound.
  • [ ] The raw double printed before any int cast — casts hide fractional rates.

When the diagnosis turns into a new station, start at the camera step:

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

For the recording side of the rig, the area scan camera range:

CamerasArea scan cameras

GigE, USB3, 10GigE and CoaXPress

1,287 models · from $149.00

Bring the numbers — clip, property value, OpenCV version — to the contact page when you want a hand with the camera side; free expert support comes with every order.

Questions

Does casting get() to int change the answer?

Yes — get() returns a double and the int cast truncates it: a 29.97 fps file prints as 29, a 23.98 fps file prints as 23, and a 0.4 fps timelapse prints as 0. Read the property into a double, print it with two decimals, and decide about rounding afterwards. A zero that appears only after the cast deserves one re-test with the raw double before you conclude the metadata is missing.

How is a variable-frame-rate file different?

Messaging apps and screen recorders usually write variable-frame-rate files: every frame carries its own timestamp and no single header rate exists, so 0 can be the honest answer. Counting frames gives you the average over the interval you measured, which is what most pipelines need. For per-frame timing, read the presentation timestamps with a media-inspection tool before the frames enter the pipeline, because VideoCapture does not expose them per frame.

Why did the same file report 90000 fps after an update?

That value is a timescale read without its tick division — a parsing bug reported between the 4.8.0.76 and 4.7.0.72 packages, where the older package returned the correct rate for the same file. Treat any value above 240 fps from a handheld clip as invalid, keep the OpenCV version beside every FPS you log, and either step the package version or measure by counting until a fixed package ships.

How long should the counting interval be?

Long enough that one frame barely moves the answer: at 120 frames the resolution is about 0.8 percent, at 300 frames about 0.3 percent. For a 30 fps file that means 4 s versus 10 s of decoding. Count at least 120 frames after a ten-frame warm-up and run it twice — two readings within 1 percent of each other are a stable number you can commit to.

Does CAP_PROP_POS_MSEC always return milliseconds?

No. In 4.5.5-era builds the first read could come back in seconds rather than milliseconds, and the behavior has changed across releases since. Test it once against a clip of known length: seek to the end, read CAP_PROP_POS_MSEC, and see which unit matches the duration. Divide by 1000 only once milliseconds are confirmed, before using frame count over duration.

Will a rebuild fix files that genuinely lack the header rate?

No. A rebuild brings your build level with the parsing support in the current source tree; it cannot create metadata the recorder never wrote. After a rebuild, a variable-frame-rate clip from a messaging app still reports 0 — and that answer is then correct, because the file has no single rate. Counting frames stays the measurement of record for those files.