ffmpegvideo-encodingtroubleshooting

FFmpeg Audio Out of Sync Isn't the Codec. It's the Frame Rate

·Javid Jamae·10 min read
FFmpeg Audio Out of Sync Isn't the Codec. It's the Frame Rate

The clip plays fine in QuickTime. You run it through FFmpeg, and by the two-minute mark the audio is a quarter second ahead of the picture. Nudging -itsoffset fixes the first ten seconds and breaks the rest, which is the tell that you're chasing the wrong variable.

Quick answer: FFmpeg audio out of sync after a transcode is almost always a variable-frame-rate source (iPhone, OBS, QuickTime screen recording, iMovie export) meeting a fixed -r on the output. Detect it by comparing r_frame_rate and avg_frame_rate in ffprobe; if they differ by more than about 1%, the source is VFR. Re-encode with -fps_mode cfr -r 30 -af aresample=async=1000:first_pts=0 rather than -r alone, and reserve -itsoffset for a fixed offset that never grows.

The drift isn't an encoder bug, it's a timestamp mismatch

Conventional wisdom says drift means the audio encode went wrong, so you raise the bitrate, force -ar 48000, or re-mux to MKV and hope. Those changes sometimes shift the symptom, which is why the advice keeps circulating on VideoHelp and the ffmpeg-user list. But the cause isn't the audio codec. Your source video has no fixed frame interval, and your command told FFmpeg to pretend it does.

A variable-frame-rate file stores a real timestamp per frame. A screen recorder emits a frame when the screen changes, so a static slide might hold for 400 ms and a scroll burst might run at 60 fps. An iPhone drops its capture rate in dim light. iMovie exports carry the same irregularity forward, which the ffmpeg-user thread on iMovie A/V sync problems describes.

Audio has no such freedom. It's a continuous stream of samples at a fixed rate, so it always plays back at real time. When the video track's reconstructed duration stops matching wall clock, the two slide apart, and the gap grows linearly. That's the signature: a constant offset is a mux problem, a growing offset is a frame-rate problem.

The -r flag makes this worse in two specific ways. Placed before -i, it's an input option that discards the source timestamps and reassigns them at a fixed cadence, so a clip that averaged 23.48 fps gets replayed as if it were 30 fps and runs short by more than 20%. Placed after, -r does drop and duplicate frames to reach a constant grid, but silently, and when the source has multi-second capture gaps the reconstruction rarely lands where the audio does.

Detect VFR before you transcode, not after

Two ffprobe fields tell you whether a file is variable frame rate, and the check takes under a second because it only reads the header.

ffprobe -v error -select_streams v:0 \
  -show_entries stream=r_frame_rate,avg_frame_rate,duration \
  -of default=noprint_wrappers=1 input.mp4

A QuickTime screen recording comes back like this:

r_frame_rate=600/1
avg_frame_rate=1409/60
duration=63.216667

r_frame_rate is FFmpeg's guess at the smallest constant rate that every timestamp in the file fits into. avg_frame_rate is total frames divided by duration. On a genuine constant-frame-rate file both read 30000/1001 and you move on. Here the first says 600 and the second says 23.48, so the container is holding fractional-frame timings that no fixed grid describes.

Now probe the audio stream on the same file:

ffprobe -v error -select_streams a:0 \
  -show_entries stream=duration -of csv=p=0 input.mp4

If that returns 63.104000 against the video's 63.216667, you already have 112 ms of mismatch before FFmpeg touches anything.

For a definitive answer, run the vfrdet filter, which measures actual frame-duration variance instead of inferring it:

ffmpeg -i input.mp4 -vf vfrdet -an -f null -

The last line of stderr reports something like VFR:0.755100 (1120/363). A value of 0.000000 means every frame interval was identical. Anything above zero is variable, and the two counts are variable-interval frames against constant ones. This is the check worth running at ingest, alongside the header validation you'd do for moov atom problems on uploaded video.

The fix chain: normalize the video grid, then repair the audio

Three flags do the work. Force a constant video grid against the real timestamps, resample the audio to fill or trim gaps, and only then consider a manual offset.

Force CFR with -fps_mode, not with -r alone

-fps_mode cfr tells FFmpeg to build a constant-rate output by duplicating or dropping frames relative to the source's actual presentation timestamps, so the output's wall-clock duration matches the input's. FFmpeg 5.1 (August 2022) added -fps_mode as the per-stream replacement for the global -vsync. On older builds, -vsync cfr does the same thing.

ffmpeg -i input.mp4 \
  -fps_mode cfr -r 30 \
  -c:v libx264 -crf 20 -preset veryfast -pix_fmt yuv420p \
  -c:a aac -b:a 128k -ar 48000 \
  -af aresample=async=1000:first_pts=0 \
  -movflags +faststart output.mp4

Keep -r 30 after -i. It sets the target grid; -fps_mode cfr sets the policy for hitting it. Without -r, CFR mode falls back to the source's guessed r_frame_rate, which on that screen recording would be 600 fps and would produce a file roughly 25 times larger than needed.

Repair gapped audio with aresample=async

aresample=async=1000 lets FFmpeg stretch or compress the audio by up to 1000 samples per second to track the video timeline, and first_pts=0 pads the head with silence when the audio starts late instead of yanking it forward. Screen recorders and browser capture tools drop audio packets when the source app stalls. Without async resampling FFmpeg writes the surviving samples back to back, so the audio finishes early by exactly the length of the dropped gaps.

The old -async 1 option maps to this filter internally and is deprecated. Write the filter directly so the behavior is visible.

Use -itsoffset only when the offset never grows

-itsoffset shifts an entire input's timestamps by a fixed amount, the right tool for a constant lag such as a camera whose microphone runs through a separate interface. It's the wrong tool for VFR drift, because one constant shift can't correct an error that accumulates.

ffmpeg -i in.mp4 -itsoffset 0.35 -i in.mp4 \
  -map 0:v -map 1:a -c copy -movflags +faststart out.mp4

That opens the same file twice, delays the second copy by 350 ms, and takes video from the first and audio from the second, no re-encode. Measure the offset at 10 seconds and again at the end of the clip first. Same number both times means a fixed offset, so -itsoffset is right. A number that has doubled sends you back to -fps_mode. The same measure-before-you-shift logic applies to subtitle delay with -itsoffset.

Concatenating VFR clips multiplies the error

Drift that's tolerable in one clip becomes obvious in a stitched video, because every join adds its own error and nothing resets it. Each VFR clip carries a small mismatch between its video and audio stream durations, often 20 to 60 ms. Join eight of them with the concat demuxer and -c copy, and the audio in the final clip can land 400 ms off, well past the roughly 100 ms threshold where viewers notice lip sync problems.

The fix is not a better concat command. Normalize every clip to the same frame rate, sample rate, and timebase first, then concatenate them with -c copy. That's the same normalize-then-copy discipline covered in FFmpeg concat with different resolutions, extended to the time axis instead of the pixel grid.

Common pitfalls

Five command mistakes cause most sync complaints, in rough order of how often they show up:

  1. Putting -r 30 before -i. This reinterprets the source instead of conforming it, and it's the fastest way to produce growing drift.
  2. Combining -c:v copy with -fps_mode cfr. Stream copy passes packets through untouched, so the fps mode is ignored with no warning and the VFR timestamps survive into your output.
  3. Trusting the container duration field. Probe the video and audio stream durations separately; the container reports the longer of the two and hides the mismatch.
  4. Adding -shortest before diagnosing. It truncates the output to the shorter stream and makes a rate problem look like a trim problem.
  5. Reaching for -itsoffset first. If the correction changes depending on where you measure, the offset isn't fixed and no single value will hold.

Manual chain versus one API call

The command above works, and if you control the source files you should just run it. It gets harder with other people's uploads, where every clip arrives from a different capture app with its own frame-rate mode and audio gaps, and a sync bug shows up as a support ticket three days later.

FFmpeg on your own boxFFmpeg Micro
VFR detectionRun `ffprobe`/`vfrdet` per upload, branch on the resultNormalized to CFR on ingest
EncoderInstall, version-pin, and watch for `-vsync` deprecationsManaged, no binaries to install
Long jobsHold a connection or build a queue yourselfSubmit a job, poll or take a webhook, download
Concat chainsNormalize each clip yourself before joiningDownstream steps inherit a constant grid
Where it runsYour serverCode, n8n, Make, Zapier, or an AI agent over MCP

FFmpeg Micro normalizes uploads to constant frame rate on ingest, so a caption burn, concat, or resize later in the chain starts from a file whose video and audio timelines already agree. The API docs show the job submit and webhook shape, and the free tier covers a few real problem clips before you decide.

Not every case belongs on an API. If you're fixing one clip on your laptop, the -fps_mode cfr command is thirty seconds of work. If your sync problem is a hardware capture chain with a genuine fixed audio delay, fix it at the source instead of correcting it on every render. And if what you need is a timeline you can scrub and nudge by hand, that's a job for an editor, not a media API.

FAQ

Why does my iPhone video play fine in QuickTime but go out of sync after FFmpeg?

QuickTime honors the per-frame timestamps in a variable-frame-rate file and paces playback to them, so the drift never appears. FFmpeg preserves that pacing only if you tell it to. A fixed -r on the output rebuilds the video on a constant grid, and any error in that reconstruction shows up as audio drift.

Does -vsync cfr still work in current FFmpeg builds?

-vsync cfr still works but is deprecated as of FFmpeg 5.1 (August 2022), which introduced -fps_mode as the per-stream replacement. Write -fps_mode cfr on anything modern, and keep -vsync cfr only for pinned older builds.

How do I tell a fixed audio offset from real drift?

Measure the audio-to-video gap twice in the same file, near the start and near the end. A fixed offset produces the same number at both points and is correctable with -itsoffset; drift produces a larger number at the end and needs -fps_mode cfr plus audio resampling.

Can I fix audio sync without re-encoding the video?

Re-muxing with -itsoffset and -c copy fixes a constant offset without re-encoding, and it takes seconds even on a long file. Variable-frame-rate drift can't be fixed by a copy, because correcting it means rebuilding the video timestamps, which needs a real encode.

What does aresample=async=1000 actually do to my audio?

The aresample filter with async=1000 lets FFmpeg add or remove up to 1000 samples per second to keep the audio timeline aligned with the video timeline, filling capture gaps with silence rather than sliding the remaining samples earlier. At 48 kHz that's a maximum correction of about 2%, inaudible on speech.

If you'd rather not run this check on every upload, point the file at FFmpeg Micro and let ingest normalize the frame rate before your captions, concat, or resize step ever sees it. Sign up free and run one of your problem clips through it.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free