Short answer
- Animation (lofi scenes): draw a cycle whose last frame leads into the first. Do not repeat frame 1 at the end; that adds a held frame at every join.
- Real footage that drifts: crossfade the last seconds into the first seconds. In our test the jump at the join fell from 112.9 to 1.4 on ffmpeg's frame difference scale, inside the normal range of the clip.
- Motion that looks natural both ways: a ping-pong loop, forward then backward. Drop the two turnaround frames from the backward half or the picture stops for a moment at each end.
- Sound: a cut that is not on a clean point clicks. Crossfade it, cut music on a bar line, and keep the loop as WAV or FLAC until the last encode: copying an AAC loop left a gap of about 11 ms at each join in our test.
- Check it: play the loop twice in one file and look at the frame difference at the join, then watch and listen to the few seconds around it.
Commands tested on our server with ffmpeg 7.0.2 on October 29, 2026, on generated test clips. Facts from the ffmpeg and Krita documentation, YouTube Help and loopcast.io, checked the same day.
1. What makes a loop seamless
A looped video has one join: the last frame of the file, followed by the first. For the join to disappear, two things must be true.
- The picture moves across the join by the same amount it moves between any two frames. If the scene is somewhere else at the end (clouds further along, a light that has changed), the viewer sees a jump. If the last frame is a copy of the first, the picture stops for one frame, which reads as a small hitch, once per loop.
- The sound continues across the join without a step. A sudden jump in the waveform plays as a click, and a short silence plays as a dropout.
Which method gets you there depends on the material:
| Material | Method | Watch out for |
|---|---|---|
| Drawn or 3D animation (a lofi room, rain on a window, a cat's tail) | Animate a cycle that ends one step before it begins | A repeated first frame; a frame rate that does not divide into the stream's |
| Filmed footage that drifts (water, clouds, fire, traffic) | Crossfade the end into the start | A double image during the fade if the camera moved |
| Motion that looks the same played backward (swaying plants, ripples, a slow zoom) | Ping-pong: forward, then reversed | Held frames at the turnarounds; anything with a direction, such as falling rain, looks wrong in reverse |
| Footage with a natural repeat (a fan, a turntable, a pendulum) | Cut on the frame before the motion repeats | Choosing the repeat frame itself as the last frame |
This guide is about the file. Making a player repeat it is a separate step: our guide to looping a video in OBS covers the Loop setting and running it 24/7 from a PC.
2. A lofi animated loop
The looping scene on a lofi stream is usually a short animation: one illustrated room with a few moving parts. The Krita manual calls the walk cycle the starting point for a reason: "Walk cycles are considered the most basic form of a full animation". A lofi loop is the same idea, a cycle of drawings that ends where it began. Three things decide whether the join shows.
End one frame before the start
If a cycle has 120 frames, frame 120 would be the same as frame 0, so the file must stop at frame 119. Exporting frame 120 as well is the most common seam fault in animation. We tested it with a generated animation that repeats every 4 seconds at 30 fps: the 120-frame file looped with no change at the join, while the 121-frame file showed a frame difference of 0.60 at the join against about 3 everywhere else, a held frame on every pass.
Count the frames in your export before you build anything from it:
ffprobe -v error -count_frames -select_streams v -show_entries stream=nb_read_frames -of csv=p=0 loop.mp4
It prints the number of frames, 120 for our clean test loop.
Pick a frame rate that fits the stream
Krita's manual gives beginners this advice: "If you are just starting out, you are best off limiting yourself to 12 frames per second. Then, a 10-second clip would be 120 frames." That works for drawing, but your stream may run at 30 fps; Loopcast's output, for example, is "1080p at 30 fps". Converting 12 fps to 30 fps cannot hold every drawing for the same time, because 30 divided by 12 is 2.5 (our arithmetic). In our test, ffmpeg's fps filter, which per its manual converts "by duplicating or dropping frames as necessary", held the drawings for an uneven mix of 3, 2 and, once, 1 frame. A 15 fps animation converted the same way held every drawing for exactly 2 frames. Animate at 10, 15 or 30 fps if the stream runs at 30.
Make the length fit the music
If the animation moves to the beat (a nodding head, a tapping foot), make the cycle a whole number of bars. As an example of the arithmetic: at 80 BPM in 4/4, 8 bars are 32 beats of 0.75 seconds, so 24 seconds; at 30 fps that is 720 frames, at 15 fps 360. If nothing moves to the beat, the length of the picture loop does not need to match the music at all; a different length keeps the two from repeating together.
To render to video, Krita's manual says "you’ll need a program called FFmpeg". Export the cycle once at the stream's frame rate, check the frame count, and use that file as the loop.
3. A crossfade loop for filmed footage
Filmed footage almost never ends where it started. The fix is to dissolve the last few seconds into the first few, so the end of the file leads into its start. ffmpeg's xfade filter does this, with one condition from its manual: "Both inputs must be constant frame-rate and have the same resolution, pixel format, frame rate and timebase." Our ambient video guide has the full command and how to set its numbers; we do not repeat it here.
We measured what it does. Our test clip was 12 seconds of slowly drifting stripes at 30 fps, which do not line up at the ends. Played twice in a row, the frame difference at the join was 112.9, against about 1.5 between other frames: a hard jump. After the ambient guide's 2-second crossfade the file was 10 seconds long, the difference at the join was 1.42, and the highest value anywhere in two passes was 1.74. The join was no longer the biggest change in the file.
Two rules make crossfades work on real footage. Use a tripod and a static shot, because a fade between two camera positions shows as a double image. And make the fade long enough for the motion: a slow fade hides a slow drift. Our guides to fireplace videos and aquarium videos cover filming for a loop.
4. A ping-pong (boomerang) loop
A ping-pong loop plays the clip forward, then backward, so it always ends where it began, with no fade. It suits motion with no obvious direction: leaves in a breeze, ripples, a slow zoom. It does not suit rain, smoke, walking people or anything else that looks wrong in reverse.
ffmpeg's reverse filter plays a clip backward. The simple version, forward plus the whole reversed clip, has a fault: the last forward frame and the first reversed frame are the same picture, and so are the last reversed frame and the first forward one. The picture stops twice per cycle. The fix is to drop the first and last frame of the reversed half. For a 12-second clip at 30 fps (360 frames):
ffmpeg -i clip.mp4 -filter_complex \
"[0:v]split[f][b];[b]reverse,trim=start_frame=1:end_frame=359,setpts=PTS-STARTPTS[r];[f][r]concat=n=2:v=1:a=0,format=yuv420p[v]" \
-map "[v]" -c:v libx264 -crf 18 -fps_mode cfr -r 30 pingpong.mp4
How it works: split makes two copies, reverse flips one, and trim keeps reversed frames 1 to 358; per the manual, start_frame is "The number of the first frame that should be passed to the output" and end_frame is "The number of the first frame that should be dropped". concat joins the forward and backward halves. For your clip, set end_frame to its frame count minus one.
What we found on October 29, 2026: the output had 718 frames (360 forward plus 358 back), 23.93 seconds. Without -fps_mode cfr -r 30 the same filter gave a file with only 599 frames, so keep that part. Played twice, the trimmed version had no held frames: every frame difference was between 1.22 and 1.76. The simple version, with the full reversed clip, showed 0.15 at the far turnaround and 0.50 at the restart, two held frames per cycle.
Two more points from the manual and our test. The reverse filter warns: "This filter requires memory to buffer the entire clip, so trimming is suggested." Keep the source short, seconds rather than minutes. And the command drops the sound (a=0) on purpose: music played backward is obvious, so give a ping-pong picture its own audio loop.
5. The audio loop seam
Most loops that look seamless are given away by the sound. Three faults cause it, and we tested each one with a 220 Hz tone, where any fault is easy to measure.
| Loop | Result at the join | What you would hear |
|---|---|---|
| WAV tone exactly 12 s long (2,640 whole cycles), copied twice | Biggest step 0.0036, the same as anywhere else; 24.000 s | Nothing |
| WAV tone cut at 10.005 s, part way through a cycle, copied twice | Biggest step 0.0705, about 20 times the normal 0.0036 (our arithmetic) | A click on every pass |
The clean 12 s tone encoded to AAC, then copied twice with -c copy | 24.021 s instead of 24.000 s; 512 samples of near silence at the join | A gap of about 11 ms (512 samples at 48 kHz, our arithmetic) on every pass |
| The clean WAV repeated first, then encoded to AAC once | 24.000 s, no step and no gap | Nothing |
What this means in practice:
- Crossfade the sound like the picture. ffmpeg's
acrossfadefilter is the audio version; its manual says "The cross fade is applied for specified duration near the end of first stream." Our fireplace guide has a tested command for a crackle loop. - Cut music on the bar. A loop of a beat, a pad or a field recording with rhythm should be a whole number of bars long (the 80 BPM example above gives 24 seconds) and start on the downbeat.
- Keep loop audio as WAV or FLAC until the last step. In our test the AAC file decoded with 512 near-silent samples of padding at its end, and copying it end to end repeated that padding at every join. Repeat the uncompressed sound first, then encode once.
- Do not reverse the sound for a ping-pong loop. Use a separate audio loop.
For a full lofi mix (many tracks rather than one loop), the seam is where the last track ends and the first begins again. Our guide to making a 24/7 lofi stream covers the mix length and that join.
6. Check the seam before you stream it
Rather than watching a player restart the file, put two passes of the loop in one file, so the join sits in the middle and only the file is being tested:
ffmpeg -stream_loop 1 -i seamless.mp4 -c copy twice.mp4
-stream_loop sets how often the input repeats; "Loop 0 means no loop, loop -1 means infinite loop", so 1 gives two passes. -c copy copies the video without re-encoding. Then measure how much each frame changes from the one before:
ffmpeg -i twice.mp4 -vf "signalstats,metadata=print:key=lavfi.signalstats.YDIF:file=ydif.txt" -an -f null -
The signalstats value YDIF is, in the manual's words, the "average of sample value difference between all values of the Y plane in the current frame and corresponding values of the previous input frame": how much the brightness changed since the last frame. The command writes one value per frame into ydif.txt. Look at the frame number equal to the loop's frame count, where the second pass starts (frame 300 for a 10-second loop at 30 fps), and compare it with its neighbours:
- Much higher than the neighbours: a jump. Our uncut test clip scored 112.9 at the join against about 1.5 around it.
- Close to zero while the neighbours are not: a held frame, usually a repeated first frame or an untrimmed ping-pong.
- In line with the neighbours: the picture seam is clean.
Then look and listen. This cuts the 6 seconds around the join of a 10-second loop into a short file you can play on repeat:
ffmpeg -stream_loop 1 -i seamless.mp4 -ss 7 -t 6 -c:v libx264 -crf 18 -c:a aac check.mp4
The join is 3 seconds into check.mp4. For your own loop, set -ss to the loop length minus 3. Listen on headphones as well as speakers.
7. One long file or a short loop
A seamless loop of a few seconds can go on air as it is, because the sender repeats it. Or you can build one long file from it. They suit different jobs.
| Compared | Short seamless loop | One long file built from loops |
|---|---|---|
| Joins per hour | Many: 360 for a 10-second loop (our arithmetic) | One per file length, plus the joins inside, which you have already checked |
| Picture and music | Repeat together, unless the sender mixes them | Picture loop and music mix can have different lengths and run past each other |
| File size | Small | Grows with length: as an example, an hour at 8 Mbps is about 3.6 GB (our arithmetic: 8 Mbps x 3,600 s / 8) |
| As a YouTube upload | Too short to be what people search for | Up to "256 GB or 12 hours, whichever is less" for a verified account |
| How viewers see it | A few seconds repeating all day is easy to spot | The repeat is minutes or hours apart |
For a stream, the long file usually wins: the visual loop can be short, while the file is as long as the music mix. Loopcast puts it bluntly on its YouTube page: "Looping a few seconds of footage over and over is the fastest way to be treated as low-effort repetitive content."
To build the long file, repeat the video loop without re-encoding, and take the sound from a WAV (or the music mix) so it is encoded once, avoiding the AAC gap from section 5:
ffmpeg -stream_loop -1 -i loop.mp4 -stream_loop -1 -i music.wav \
-map 0:v -map 1:a -t 3600 -c:v copy -c:a aac -b:a 192k -movflags +faststart one-hour.mp4
-t 3600 stops after one hour. We ran it with -t 60 on a 4-second picture loop and a 12-second tone: the result was 60 seconds of H.264 and AAC with the index at the start of the file, every picture join between 2.84 and 3.17 on the YDIF scale (the clip's normal range), and no step or gap in the sound at the 12 and 24 second joins. If the loop's frame rate differs from the stream's, re-encode the video once at the stream's rate instead of copying it.
Before you put the loop on air
- The last frame leads into the first and is not a copy of it; the frame count is what you expect.
- An animation runs at 10, 15 or 30 fps for a 30 fps stream.
- A crossfade was made from a static shot; a ping-pong has its turnaround frames trimmed and no reversed sound.
- The sound is crossfaded or cut on the bar, and kept uncompressed until the final encode.
- In a two-pass file, YDIF at the join is in line with its neighbours.
- You watched and listened to the seconds around the join on headphones.
- The file on air is long enough that the repeat is not obvious.
FAQ
How do I make a seamless looping video that doesn't jump?
Make the end of the file lead into its start: animate a cycle, crossfade the last seconds into the first, or play the clip forward and back. Then check the join in a file with two passes of the loop, so you see the real seam rather than your player restarting.
Why does my loop stutter at the restart even though the frames match?
Usually the last frame is a copy of the first, so the same picture shows twice. Drop the last frame. In a ping-pong loop, drop the first and last frame of the reversed half.
How do I loop a video back and forth?
Join the clip with a reversed copy of itself. The ffmpeg command in this guide does it with the reverse, trim and concat filters and removes the two repeated turnaround frames.
Why is there a click or gap when my audio loops?
A click comes from cutting the sound at a point where the waveform does not meet its start; crossfade it or cut on the bar. A short gap can come from copying an AAC file end to end; repeat the uncompressed sound and encode once.
What frame rate should a lofi loop animation use?
One that divides evenly into the stream's rate. For a 30 fps stream, 10, 15 or 30 fps. A 12 fps animation converted to 30 fps holds drawings for uneven lengths.
Sources
- ffmpeg documentation: filters (xfade, acrossfade, reverse, trim, fps, signalstats, metadata) and ffmpeg tool (-stream_loop). Checked on October 29, 2026.
- Krita manual: Introduction to animation. Checked on October 29, 2026.
- YouTube Help: upload videos longer than 15 minutes. Checked on October 29, 2026.
- All commands tested on our server with ffmpeg 7.0.2 on October 29, 2026, on generated test clips (drifting stripes, a periodic animation and a 220 Hz tone). Nothing was uploaded to YouTube or Loopcast.
- Loopcast: YouTube page and pricing. Checked on October 29, 2026.