ffmpegtroubleshootingvideo-processing

Non-monotonous DTS Isn't Noise. It's Truncating Your Joins.

·Javid Jamae·10 min read
Non-monotonous DTS Isn't Noise. It's Truncating Your Joins.

Your concat job runs, FFmpeg prints a hundred lines of Non-monotonous DTS in output stream 0:1, exits 0, and hands back a file that's 45 seconds long when the parts add up to three minutes. The usual advice is -fflags +genpts. That flag is correct for one of the four causes. On the other three it hides the damage until an HLS packager or a social upload rejects the output.

Quick answer: "Non-monotonous DTS" means FFmpeg's muxer received a packet whose decode timestamp is lower than or equal to the previous packet's. In production that comes from joining segments whose timestamps never restart, or from remuxing a live source with a jittery clock. Use -fflags +genpts only when the input has no PTS at all, put -reset_timestamps 1 on the split (a segment-muxer option, not a concat one), use -af aresample=async=1 when the offending stream is audio, and re-encode to constant frame rate with -fps_mode cfr -r 30 when the source is broken. If you'd rather not tune muxer flags per file, FFmpeg Micro normalizes and joins clips as one API call with no encoder to host.

What the muxer is actually complaining about

DTS is the decode timestamp: the order the decoder needs packets in, which differs from PTS (presentation order) when the codec uses B-frames. Every FFmpeg muxer requires DTS to strictly increase within a stream, so a packet arriving at or below its predecessor breaks the container's contract.

The CLI doesn't stop. It bumps the offending packet to previous + 1 tick and warns:

[mp4 @ 0x7f8e] Non-monotonous DTS in output stream 0:1;
previous: 1234567, current: 1234000; changing to 1234568.
This may result in incorrect timestamps in the output file.

On an MP4 with a 90,000 timescale, previous + 1 gives that frame a duration of about 11 microseconds. Do that a few thousand times across a join and the container's duration field drifts far from the real content. Newer builds spell it "Non-monotonic DTS in output stream", so grep for both.

A second variant, Application provided invalid, non monotonically increasing dts to muxer in stream 0: 1234567 >= 1234000, comes from libavformat when you drive the API directly (fluent-ffmpeg, PyAV, a Go wrapper), because the auto-correction lives in the CLI's muxer wrapper, not the library.

The part that costs teams real time: this is a warning. FFmpeg exits 0. Your n8n node goes green, your Lambda returns success, and nothing knows the file is wrong.

The two situations that actually produce it in a pipeline

Forum threads about this are usually someone converting one file on a laptop. In a pipeline it comes from two places that need different fixes.

Joining segments whose timestamps never restart

-reset_timestamps 1 is an option of the segment muxer, not of the concat demuxer. Half the commands you'll find paste it after -i on the join, where it does nothing. It belongs on the split:

ffmpeg -i long.mp4 -c copy -f segment -segment_time 60 \
  -reset_timestamps 1 part_%03d.mp4

With that flag, every part starts at 0:00. Without it, part_003.mp4's first packet sits at 120 seconds because the segments keep the source timeline. Anything that joins them assuming files start at zero (the concat: protocol, MPEG-TS parts, a hand-assembled HLS playlist) sees DTS run backwards at each boundary.

The concat demuxer usually compensates for per-file start times, which is why the same segments join cleanly one way and warn on another. Mixed sources are worse: rounding between a 90,000-tick source and a 15,360-tick output can produce a duplicate DTS on its own. Our normalize-then-copy guide for mismatched resolutions covers the geometry side.

Remuxing a stream whose source clock jitters

RTSP cameras, RTMP ingests, and cheap encoders send timestamps from hardware clocks that stall, wrap, or reset. The bluenviron/mediamtx project has a long-running discussion titled "many Non-monotonous DTS errors while streaming". The fix is on the input side, not the muxer:

ffmpeg -rtsp_transport tcp -use_wallclock_as_timestamps 1 \
  -i rtsp://cam.local/stream1 \
  -c copy -f mp4 -movflags +faststart out.mp4

-rtsp_transport tcp removes the packet loss UDP delivery causes on a busy network, a common source of the jitter itself. -use_wallclock_as_timestamps 1 discards the camera's clock and stamps packets with the receiving machine's wall clock. Blunt, and right when you don't control the encoder.

Find the backwards jump before you change any flags

Guessing is how people end up with six flags and no idea which one matters. Find the packet first.

  1. Print the decode timestamps and flag anything that doesn't increase:
ffprobe -v error -select_streams v:0 -show_entries packet=dts_time \
  -of csv=p=0 part_003.mp4 \
| awk 'NR>1 && $1+0 <= prev+0 {print "backwards at packet " NR ": " $1 " after " prev} {prev=$1}'
  1. Run it on a:0 as well. If the video is clean and the audio isn't, you have the audio variant and no video flag will help.
  2. If dts_time prints N/A, the stream has no decode timestamps at all. That, and only that, is the case -fflags +genpts was built for.

Note the stream index. 0:1 is the second output stream, usually the audio track, and people lose an afternoon to video flags because they never read it.

FFmpeg's status line is the other signal people scroll past. frame=5400 fps=210 q=-1.0 dup=12 drop=318 means it threw away 318 frames to keep the timeline plausible, and Past duration 0.999992 too large repeated thousands of times means the input is variable frame rate.

The decision tree

Every flag below fixes one specific cause and quietly corrupts something when applied to the others. Match it to what your ffprobe output showed.

What you foundWhat to useWhy it's the right one
`dts_time` is `N/A` (raw streams, some MPEG-TS)`-fflags +genpts` before `-i`Generates presentation timestamps from packet order. It's an input option; after `-i` it's ignored
Segments carry the source timeline`-reset_timestamps 1` on the split commandEach part starts at zero, so joiners don't see time run backwards
Only the audio stream trips the warning`-af aresample=async=1:first_pts=0`Inserts or drops samples so audio matches its timestamps, instead of letting the muxer nudge each packet
First timestamps are negative (encoder priming, live capture)`-avoid_negative_ts make_zero`Shifts the whole track so it starts at 0 rather than clipping
Everything above tried, source is a mess`-fflags +igndts`Last resort. Discards DTS and keeps PTS only, so the muxer invents decode order. Breaks B-frame stream copy
DTS jumps around mid-file with real gapsRe-encode with `-fps_mode cfr -r 30`Rebuilds a clean timeline. The only reliable fix, and the only one that costs CPU

-fflags +igndts gets cargo-culted hardest because the warnings disappear. With H.264 or HEVC stream copy it also throws away the decode order the codec depends on, so the file plays in VLC and stutters or freezes on iOS and in browsers. Re-encode instead.

-vsync was deprecated in FFmpeg 5.1 in favor of -fps_mode. Any answer still telling you to use -vsync cfr predates that change, and probably others.

What a silently truncated output looks like downstream

The output is usually playable, which is why this survives testing. VLC decodes the whole file and ignores a bad duration header, so it's the least reliable place to test. What breaks is everything that reads container metadata:

ffprobe -v error -show_entries format=duration -of csv=p=0 out.mp4
ffprobe -v error -select_streams v:0 -show_entries packet=dts_time \
  -of csv=p=0 out.mp4 | tail -1

If the format duration says 45 and the last packet says 178, the moov atom is lying. Players draw a 45-second scrub bar. Transcription services read the header and stop early, so captions cover the first quarter. An HLS packager writes an #EXTINF of 0.011 for each nudged frame, and players stall there. Platforms that validate duration reject the upload with a message that has nothing to do with timestamps. If you're building segments, the HLS walkthrough shows what correct segment durations look like, and validating uploads for a missing moov atom catches related ingest damage before FFmpeg runs.

Normalize, then join

For a genuinely broken source, no muxer flag saves you. Rebuild each part on one clean timeline, then stream-copy the join, which is fast because the expensive work already happened:

ffmpeg -i part1.mp4 \
  -c:v libx264 -crf 20 -preset veryfast \
  -fps_mode cfr -r 30 -video_track_timescale 30000 \
  -c:a aac -ar 48000 -ac 2 -af aresample=async=1:first_pts=0 \
  -movflags +faststart norm1.mp4

Run that for every part with identical settings, then join:

printf "file 'norm1.mp4'\nfile 'norm2.mp4'\n" > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4

Identical is the operative word. Same frame rate, same timescale, same sample rate, same channel count. One part at 44,100 Hz among a set at 48,000 Hz brings the audio warnings straight back.

It's also the part nobody wants to own: an encoder to host, per-file flag decisions, and a job longer than most serverless timeouts. FFmpeg Micro does the normalize-and-join as one job. You post the source URLs, it returns a joined MP4 on a clean CFR timeline, and you take a webhook when it's done. The job shape is in the docs, and it works the same from code, from n8n, Make, and Zapier, or from an AI agent over MCP.

When this isn't the right approach

Re-encoding every part is wasteful if the parts already share a codec, frame rate, and timebase. Check with ffprobe first: a clean stream copy is close to free, and a CFR re-encode of an hour of 1080p is not.

If you're restreaming 24/7 rather than processing files, a per-stream FFmpeg process is the wrong architecture regardless of flags. That's a media server's job, and DTS warnings there point at the source encoder, fixable at the camera.

And if a recorder dropped out and left multi-second gaps, no timeline fix invents the missing content. You get a jump cut, so decide that deliberately instead of discovering it in the output.

FAQ

Is "Non-monotonous DTS in output stream" an error or a warning?

"Non-monotonous DTS in output stream" is a warning, and FFmpeg exits with code 0 after printing it. That's why pipelines mark the step successful and ship a file whose container duration doesn't match its content. Parse stderr for the string if you need the job to fail, because the exit code won't tell you.

Does -fflags +genpts fix non-monotonous DTS on a concat?

-fflags +genpts only helps when the input packets have no presentation timestamps at all, which ffprobe -show_entries packet=dts_time confirms by printing N/A. On a concat of normal MP4 files that already carry timestamps, genpts changes nothing and the warnings keep coming. It also has to appear before -i, since it's an input option.

Why do I only get the DTS error on the audio stream?

Audio trips it more often because audio packets are short, so a clock that drifts by a few milliseconds crosses a whole packet boundary and lands on a duplicate DTS. Fix it with -af aresample=async=1:first_pts=0, which lets the resampler add or drop samples to match the timeline. That means re-encoding the audio, but AAC at 128 kbps is cheap next to a video pass.

If you'd rather stop maintaining a decision tree of muxer flags, the free tier covers enough jobs to run your next batch of joins through the API and compare durations against your current command's output.

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