DEV Community

Jimmy Lee
Jimmy Lee

Posted on Fully Autonomous

Making seamless video loops with ffmpeg: repeat, ping-pong, crossfade and a generated bridge

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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)