We shipped a feature where sellers upload a short video for each product. It worked in testing and behaved strangely in the wild: the first play took an age on mobile data, pausing and resuming sometimes jumped, and dragging the scrubber did nothing at all until the video had been playing for a while.
I spent a day suspecting the player. It was the files.
The index is at the end
An MP4 is a tree of boxes. The one that matters here is moov — the movie header, which holds the index: where every frame lives, the timing, the track layout. Without it a player knows nothing; with it, a player can jump anywhere.
Most encoders write moov last, because until the final frame is encoded they do not know the sizes to put in it. For a file you open from disk that is irrelevant. For a file streamed over HTTP it is the whole problem: the player must fetch to the end of the file before it can render the first frame or honour a seek.
On a good connection this is invisible. On a phone on mobile data, with a 40 MB clip, it is ten seconds of nothing, a scrubber that does not work, and a "pause" that stalls when you resume because the buffer is still filling linearly from wherever it got to.
The flag
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4-movflags +faststart relocates moov to the front of the file. -c copy means the audio and video streams are copied through untouched — this is a remux, not a re-encode. No quality loss, no long CPU job; ffmpeg rewrites the container and shuffles one box.
The cost is that it needs a second pass over the file to do the move, so it is roughly disk-speed rather than free. On a 40 MB clip that is well under a second.
That single change fixed the slow first frame and the broken seeking outright.
The second problem: where the keyframes are
Pausing and resuming still felt loose on some clips, and scrubbing snapped to strange places. That is a different cause: keyframe spacing.
A player can only start decoding at a keyframe. If a recorder emits one every ten seconds, every seek lands on the nearest keyframe up to ten seconds away from where the user actually pointed, and resuming after a pause can stutter while the decoder reassembles state.
You cannot fix keyframe spacing with a remux — it is inside the encoded stream — so this is the case where a real re-encode is justified:
ffmpeg -i input.mp4 \
-c:v libx264 -preset veryfast -crf 23 \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -ac 2 \
-movflags +faststart \
output.mp4-g 60 with -keyint_min 60 and -sc_threshold 0 forces a keyframe every 60 frames — two seconds at 30 fps — and stops ffmpeg inserting extra ones at scene changes, which would otherwise make the spacing unpredictable. Seeks are now accurate to two seconds, and the file is still compatible with everything.
-preset veryfast is a deliberate choice. On a server processing user uploads, encoder time is latency the user feels; veryfast at crf 23 is visually fine for phone-shot product clips and several times quicker than medium.
Decide which one you need
Re-encoding every upload is wasteful, and it degrades files that were already fine. Probe first and re-encode only when you must:
ffprobe -v error \
-select_streams v:0 \
-show_entries stream=codec_name,width,height,r_frame_rate \
-of default=noprint_wrappers=1 \
input.mp4The rule we settled on:
- H.264 video, AAC audio, sane dimensions → remux with
+faststart. Cheap, lossless, covers most iPhone and modern Android recordings. - Anything else — HEVC that older Android will not decode, oddball audio codecs, rotation metadata some players ignore, absurd resolutions — → full re-encode with the settings above.
Get the poster frame in the same job
While the file is already open, extract the still you will show before the video is ready. In a scrolling feed this is the single biggest perceived-performance win you can buy, and it costs one more command:
ffmpeg -ss 00:00:01 -i input.mp4 -frames:v 1 -q:v 3 poster.jpgPutting -ss before -i makes ffmpeg seek before decoding instead of decoding everything up to that point — on a long file the difference is seconds.
One second in, rather than zero, because the first frame of a phone recording is very often a blurred smear from the camera still focusing.
Do it on the server
The temptation is to remux on the phone before uploading, and save the bandwidth. Resist it: you burn the user's battery, you add seconds to a flow they are already impatient with, and you now maintain two transcode paths that will drift apart.
Accept the original, return a 202, and process in a worker. Keep the original file — when you get the encoding profile wrong, and you will, you want to be able to reprocess rather than ask a thousand sellers to re-upload.
The short version
- Always run
-movflags +faststarton anything served over HTTP. - Remux when you can, re-encode only when the codec forces it.
- Force keyframe spacing if you re-encode; two seconds is a good default.
- Extract a poster frame in the same job.
- Do it server-side, asynchronously, and keep the original.
None of this is advanced video engineering. It is one metadata box and one flag — and it is the difference between a feed that feels instant and one users assume is broken.