Disclosure: I build LoopVideo, a browser-based tool for making video loops. The ffmpeg techniques below work on their own, with or without it. This post was written with the help of AI tools.
A looping video usually "jumps" at one place: where the last frame is followed by the first frame again. If those two frames don't match, viewers notice the cut on every cycle. This post walks through four ways to deal with that seam using plain ffmpeg, from the simplest to the most involved.
0. Normalize first
Most loop problems get worse when you concatenate clips with different frame rates, sizes or pixel formats. Normalize the source once and use that file for everything that follows, including any frame extraction:
ffmpeg -i input.mp4 \
-vf "fps=30,scale=1280:-2,format=yuv420p" \
-c:v libx264 -crf 18 -an normalized.mp4
1. Plain repeat
If the clip already starts and ends on similar frames (a spinning object, a looping animation), repeating it is enough:
# -stream_loop 2 plays the input 3 times in total
ffmpeg -stream_loop 2 -i normalized.mp4 -c copy repeated.mp4
Fast and lossless, but it does nothing to hide a visible seam.
2. Ping-pong (boomerang)
Play the clip forward, then backward. The end of the forward pass and the start of the reverse pass are the same frame, so there is no jump; the trade-off is that motion visibly reverses.
ffmpeg -i normalized.mp4 -filter_complex \
"[0:v]reverse[r];[0:v][r]concat=n=2:v=1[out]" \
-map "[out]" pingpong.mp4
reverse buffers the whole clip in memory, so keep this to short clips. It works well for water, smoke, slow camera drifts and other motion that looks natural in both directions.
3. Crossfade the end into the start
For a 6-second clip, take the first second off, then fade the end of the remaining clip into that first second. The output starts at the 1-second mark and ends exactly at the 1-second mark, so the loop point lines up:
# D = clip duration (6s), F = fade length (1s), offset = D - 2F = 4
ffmpeg -i normalized.mp4 -filter_complex \
"[0:v]split[a][b];\
[a]trim=start=1,setpts=PTS-STARTPTS[main];\
[b]trim=0:1,setpts=PTS-STARTPTS[head];\
[main][head]xfade=transition=fade:duration=1:offset=4[out]" \
-map "[out]" crossfade-loop.mp4
This hides the cut for textures and ambient footage. With a moving subject you may see ghosting during the fade.
4. Generate a bridge from the last frame back to the first
When the start and end are too different for a fade, you can generate a short transition clip that begins at the last frame and ends at the first frame, then append it. The pieces you need from ffmpeg are the two boundary frames and a final concat.
Extract the first frame, and the actual final decoded frame rather than a frame at an estimated timestamp. -update 1 keeps overwriting the image so the file ends up holding the very last frame:
ffmpeg -i normalized.mp4 -frames:v 1 first.png
ffmpeg -sseof -0.5 -i normalized.mp4 -update 1 last.png
Use the same normalized file you will concatenate later; extracting from the original upload can give you a frame that does not exist in the final timeline.
Feed last.png (start) and first.png (end) to an image-to-video model that supports start and end frames, get back a short clip, and join it:
ffmpeg -i normalized.mp4 -i bridge.mp4 -filter_complex \
"[1:v]fps=30,scale=1280:720,format=yuv420p,setsar=1[t];\
[0:v]setsar=1[v];[v][t]concat=n=2:v=1[out]" \
-map "[out]" seamless.mp4
(Scale the bridge to the same size as normalized.mp4.) Watch the result at least twice and look for warped details or a speed change around the join.
Doing this in the browser
ffmpeg also runs in the browser via ffmpeg.wasm, which is how LoopVideo works: the standard repeat preview and the ordinary tools (resize, crop, change speed, reverse, video-to-GIF) process the file in your browser, with no sign-in and no credits. The optional AI bridge from step 4 is different: it requires a signed-in, verified account, sends the two boundary frames to a third-party video-generation provider with a fixed prompt, and joins the returned transition to your clip in the browser. Each run uses one credit or the free daily attempt; free results are watermarked, and a failed run gives the credit or attempt back.
I wrote up the product-side walkthrough in How to Make a Video Loop Seamlessly.
If you'd rather stay on the command line, the commands above are everything you need. If you want to try the browser version: loopvideo.app.
Top comments (0)