Meet Modi
Back to Blog
·6 min·updated

ffmpeg was decoding the whole lecture to cut the last 30 seconds out of it

Cutting the last chunk of a 78 minute lecture took 23 seconds. Moving -ss before -i made every cut take about 1.5, with the audio provably unchanged.

By Meet Modi
FFmpegMedia ProcessingPerformancePythonDebugging

Putting -ss after -i is output seeking: ffmpeg decodes the file from the beginning and discards everything before your start point, so the cost grows with how far in you cut. Putting -ss before -i is input seeking, which jumps via the container index and takes roughly constant time. On a 78 minute lecture this took the last chunk from 23 seconds to about 1.5.

How the cost showed up

A transcription service splits each lecture into chunks and transcribes them in parallel. The split itself was supposed to be the cheap part, and the timings per chunk looked like this:

78-minute lecture, 10 chunks, -ss after -i

chunk  1   0.9 s
chunk  2   3.1 s
chunk  3   5.4 s
...
chunk 10  23.0 s      <- the last chunk of every long lecture

A straight line, which is the giveaway. A constant-cost operation does not get steadily more expensive the further into the file it happens. Something was scaling with the offset.

Two seeks with the same flag

ffmpeg's argument order is not cosmetic. Options apply to whatever file they precede, so -ss means different things on either side of -i.

# Output seeking: decode from 0, throw away 70 minutes, then write.
ffmpeg -i lecture.mp4 -ss 4200 -t 480 chunk10.wav

# Input seeking: use the container index to jump, then decode.
ffmpeg -ss 4200 -i lecture.mp4 -t 480 chunk10.wav

In the first, ffmpeg opens the file and runs the decoder from frame zero, discarding every decoded frame until it reaches 4200 seconds. The work is real and the output is thrown away. In the second it consults the container's index, moves the read head close to the target, and starts decoding there.

Proving the audio didn't change

This is the part I would push on if someone showed me this change. A 15x speedup on a decode is suspicious, and "it still sounds fine" is not a check. If input seeking were quietly landing on a keyframe boundary, chunks would be shifted by a fraction of a second, the transcript would lose or duplicate words at every boundary, and nobody would notice until a student complained about one lecture.

So I cut the same lectures both ways and compared the output directly.

  • Two lectures, 78 minutes and 36 minutes, 10 chunks each.
  • Chunk durations identical both ways.
  • Sample alignment within 0.4 ms, cross-correlation at or above 0.9998.
  • Transcripts byte-identical everywhere the model was confident; the only differences were in passages where it is unsure between runs anyway.
# The check, roughly. Correlate the two renderings of the same chunk.
a, sr = librosa.load("out_seek.wav", sr=None)
b, _  = librosa.load("in_seek.wav",  sr=None)
n = min(len(a), len(b))
corr = np.corrcoef(a[:n], b[:n])[0, 1]
assert corr >= 0.999, corr

Why the slow version is so easy to write

Mostly because it reads correctly. "Take this input, seek to here, write that long" is the order the sentence goes in English, and the fast version puts the seek before you have said what you are seeking in.

And it is invisible at small scale. On a three minute clip the difference is milliseconds. You only find it on long inputs, which in our case meant production lectures and not a single one of the test fixtures.

Two gotchas that come with the change

Timestamps reset to zero with input seeking, so -t and -to become equivalent, and any downstream code reading absolute timestamps needs -copyts to keep the originals. We cut to standalone chunks and track offsets ourselves, so this did not apply, but it is the thing that breaks for people who hit it.

The second is that fixing this made a different problem visible. Once each cut was cheap, we ran far more of them at once, and that turned into hundreds of ffmpeg processes and a load average of 800. Removing a bottleneck does not remove the work. It moves the queue somewhere you were not watching.

More Posts