Skip to main content
Your captions were fine at the start and a second late by minute three. Or a clip cut cleanly in your editor and landed a beat off after export. Or a pose model returned confident nonsense on footage that looks perfectly normal when you play it. These usually have one cause, and it is not the tool you are blaming. It is the video.

What “30fps” usually means, and sometimes does not

A video file declares a frame rate. Most software reads that number and assumes every frame is spaced evenly — at 30fps, one frame every 33.33 milliseconds, forever. That assumption holds for most footage. It does not hold for a lot of what phones produce. A variable frame rate file has uneven gaps between frames. It still declares a rate, and it still plays correctly, but the declaration describes the maximum rather than the rhythm. Here is a real clip:
r_frame_rate is what the file declares: 60fps. avg_frame_rate is what it achieved: 7200 ÷ 239 = 30.13fps. The file says sixty and delivered thirty. That gap is the tell. On constant-rate footage the two match exactly:

Why it drifts

Play a variable-rate clip and it looks fine, because a player reads each frame’s timestamp and shows it when the timestamp says to. Players do not assume; they follow instructions. Trouble starts when something counts instead of reading. If a tool assumes 30fps and you hand it 1800 frames, it concludes the clip is 60 seconds long. If those frames were actually spread across 63 seconds, everything positioned by frame number is now three seconds out by the end — and only at the end. The error accumulates. That is the shape of the symptom people notice: fine at the start, worse as it goes on. A fixed offset means something else. Drift that grows means the timebase disagrees with the timeline.

Why phones do this

Not a defect, and not a bug you can report. It is a deliberate trade:
  • Low light. The sensor needs longer per frame to gather enough light, so the camera quietly drops the rate rather than give you a dark video.
  • Heat. Sustained recording warms the phone; frame rate is one of the first things throttled to keep it going.
  • Battery and load. Recording while something else works the CPU produces the same effect.
The camera is choosing a watchable video over a mathematically tidy one, and for watching, it is right. The file only becomes a problem when it reaches software that measures rather than plays.

Whether this is your problem

The declared-versus-achieved check above is quick and not conclusive — a clip with one stall in a steady stream averages close to its declared rate and is still variable. To see the rhythm itself, look at the gaps between frame timestamps:
Constant-rate footage gives you one number:
The variable clip from earlier gives you two, alternating — capture in bursts, then a pause:
Two caveats, because naive versions of this check get it wrong: Sort by presentation time, not file order. Compressed video stores frames out of order — a B-frame is encoded after frames it appears before. Comparing timestamps in storage order makes perfectly constant footage look variable. The sorted() above is doing that work. Sample more than the beginning. A phone that only stutters once it warms up records the first thirty seconds perfectly. Reading a prefix and stopping will miss it entirely, no matter how long a prefix you read — the blind spot is positional, not statistical. Sample from a few places across the clip.

What actually breaks

Not everything, and knowing which is which saves work: The pattern: anything that plays the video is fine, anything that measures it is exposed. A wrong answer from a measurement is worse than a visible failure, because nothing throws an error — the numbers are simply off, and they look like numbers.

Fixing it

Conform the video to a constant rate. Frames get duplicated or dropped so the spacing becomes even:
Which rate you choose matters more than it looks. The instinct is to use the average — the file achieved 30.13fps, so conform to 30. That is usually wrong. The average is dragged down by whatever the camera dropped. A clip that shot 60 and stuttered to an average of 30 is not 30fps footage; it is 60fps footage with gaps. Conform it to 30 and you throw away half of what was actually captured, permanently, in the name of fixing it. Use the declared rate — r_frame_rate — which is the rate the camera was aiming for. Every captured frame then lands on a grid it fits, and the gaps get filled by duplication rather than the real frames being discarded. One exception worth knowing: some containers declare an absurd r_frame_rate (1000fps is a common placeholder). If the declared rate is not a plausible capture rate, fall back to the nearest standard rate above the average.

Checking that the fix worked

Re-run the gap check on the output. You want one distinct gap:
Duration should be unchanged within about one frame, and the frame count will have changed — that is the fix working, not a problem. Conforming 400 unevenly-spaced frames to a constant 30fps over 20 seconds produces 600 frames. The extra ones are duplicates filling gaps where the camera captured nothing.
FastDrop checks for this — and the other defects that behave like it — in one API call, and can perform the fix and verify the result. Why check footage before you process it covers what it looks for and why the same clip can be fine for one job and unusable for another.