<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jimmy Lee</title>
    <description>The latest articles on DEV Community by Jimmy Lee (@jimmylee0252).</description>
    <link>https://dev.to/jimmylee0252</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3851828%2F80f0352e-25e4-4d32-82c8-a79e3cdee75b.jpg</url>
      <title>DEV Community: Jimmy Lee</title>
      <link>https://dev.to/jimmylee0252</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jimmylee0252"/>
    <language>en</language>
    <item>
      <title>Making seamless video loops with ffmpeg: repeat, ping-pong, crossfade and a generated bridge</title>
      <dc:creator>Jimmy Lee</dc:creator>
      <pubDate>Thu, 01 Oct 2026 03:41:27 +0000</pubDate>
      <link>https://dev.to/jimmylee0252/making-seamless-video-loops-with-ffmpeg-repeat-ping-pong-crossfade-and-a-generated-bridge-41jm</link>
      <guid>https://dev.to/jimmylee0252/making-seamless-video-loops-with-ffmpeg-repeat-ping-pong-crossfade-and-a-generated-bridge-41jm</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://loopvideo.app/" rel="noopener noreferrer"&gt;LoopVideo&lt;/a&gt;, 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.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  0. Normalize first
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=30,scale=1280:-2,format=yuv420p"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 18 &lt;span class="nt"&gt;-an&lt;/span&gt; normalized.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Plain repeat
&lt;/h2&gt;

&lt;p&gt;If the clip already starts and ends on similar frames (a spinning object, a looping animation), repeating it is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# -stream_loop 2 plays the input 3 times in total&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-stream_loop&lt;/span&gt; 2 &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt; copy repeated.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fast and lossless, but it does nothing to hide a visible seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ping-pong (boomerang)
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[0:v]reverse[r];[0:v][r]concat=n=2:v=1[out]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[out]"&lt;/span&gt; pingpong.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reverse&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Crossfade the end into the start
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# D = clip duration (6s), F = fade length (1s), offset = D - 2F = 4&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[0:v]split[a][b];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   [a]trim=start=1,setpts=PTS-STARTPTS[main];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   [b]trim=0:1,setpts=PTS-STARTPTS[head];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   [main][head]xfade=transition=fade:duration=1:offset=4[out]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[out]"&lt;/span&gt; crossfade-loop.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hides the cut for textures and ambient footage. With a moving subject you may see ghosting during the fade.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generate a bridge from the last frame back to the first
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Extract the first frame, and the &lt;strong&gt;actual final decoded frame&lt;/strong&gt; rather than a frame at an estimated timestamp. &lt;code&gt;-update 1&lt;/code&gt; keeps overwriting the image so the file ends up holding the very last frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.mp4 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 first.png
ffmpeg &lt;span class="nt"&gt;-sseof&lt;/span&gt; &lt;span class="nt"&gt;-0&lt;/span&gt;.5 &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.mp4 &lt;span class="nt"&gt;-update&lt;/span&gt; 1 last.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Feed &lt;code&gt;last.png&lt;/code&gt; (start) and &lt;code&gt;first.png&lt;/code&gt; (end) to an image-to-video model that supports start and end frames, get back a short clip, and join it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; bridge.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[1:v]fps=30,scale=1280:720,format=yuv420p,setsar=1[t];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   [0:v]setsar=1[v];[v][t]concat=n=2:v=1[out]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[out]"&lt;/span&gt; seamless.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Scale the bridge to the same size as &lt;code&gt;normalized.mp4&lt;/code&gt;.) Watch the result at least twice and look for warped details or a speed change around the join.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing this in the browser
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I wrote up the product-side walkthrough in &lt;a href="https://loopvideo.app/blog/how-to-make-a-video-loop-seamlessly" rel="noopener noreferrer"&gt;How to Make a Video Loop Seamlessly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd rather stay on the command line, the commands above are everything you need. If you want to try the browser version: &lt;a href="https://loopvideo.app/" rel="noopener noreferrer"&gt;loopvideo.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Payasa Tattoo Generator</title>
      <dc:creator>Jimmy Lee</dc:creator>
      <pubDate>Mon, 30 Mar 2026 15:11:07 +0000</pubDate>
      <link>https://dev.to/jimmylee0252/payasa-tattoo-generator-2mj0</link>
      <guid>https://dev.to/jimmylee0252/payasa-tattoo-generator-2mj0</guid>
      <description>&lt;p&gt;Payasa Tattoo Generator brings the rich tradition of Chicano tattoo art into the AI age. Whether you're exploring design ideas or a tattoo artist looking for inspiration, this tool generates authentic payasa and Chicano-style tattoo designs in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes it different:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Culturally grounded&lt;/strong&gt;: Every design draws from authentic Chicano art elements — smile now cry later masks, sugar skulls, roses, script lettering, and the iconic payasa figure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20+ style presets&lt;/strong&gt;: From black &amp;amp; grey realism to neo-traditional, sleeve compositions to minimalist pieces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sketch-to-design&lt;/strong&gt; (Pro): Upload your own sketch and let AI transform it into a polished Chicano design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free to try&lt;/strong&gt;: Generate 3 designs per day, no account needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who it's for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People exploring tattoo ideas before visiting an artist&lt;/li&gt;
&lt;li&gt;Tattoo artists looking for reference and inspiration&lt;/li&gt;
&lt;li&gt;Anyone interested in Chicano art and culture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with Next.js and deployed on Cloudflare's edge network for fast generation worldwide.&lt;/p&gt;

&lt;p&gt;Here: &lt;a href="https://payasatattoo.shop" rel="noopener noreferrer"&gt;https://payasatattoo.shop&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>design</category>
    </item>
  </channel>
</rss>
