<?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>Forem: Lukáš</title>
    <description>The latest articles on Forem by Lukáš (@bladerik).</description>
    <link>https://forem.com/bladerik</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1325181%2F2485ec8e-c7c3-48a5-87e2-3103828de22c.jpeg</url>
      <title>Forem: Lukáš</title>
      <link>https://forem.com/bladerik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bladerik"/>
    <language>en</language>
    <item>
      <title>90% of SaaS is Doomed. Here is How Developers Can Survive the Agent Era.</title>
      <dc:creator>Lukáš</dc:creator>
      <pubDate>Fri, 27 Feb 2026 14:51:53 +0000</pubDate>
      <link>https://forem.com/bladerik/90-of-saas-is-doomed-here-is-how-developers-can-survive-the-agent-era-1c9i</link>
      <guid>https://forem.com/bladerik/90-of-saas-is-doomed-here-is-how-developers-can-survive-the-agent-era-1c9i</guid>
      <description>&lt;p&gt;Software is changing forever. In the near future, people will use AI agents to do almost everything. I believe &lt;strong&gt;90% of traditional SaaS apps are doomed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post, I will show you why the User Interface is dying. I will also share what we as developers need to build today to survive the AI revolution. Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Death of UI
&lt;/h2&gt;

&lt;p&gt;One of the main reasons why agents will take over is very simple. &lt;strong&gt;It comes down to convenience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For years, we built user interfaces. We tried our best to make them user-friendly. But what is a UI really? It is just a bridge. It displays data and allows the user to do stuff.&lt;/p&gt;

&lt;p&gt;Learning dozens of different tools is exhausting. Speaking to an AI to do the work for you is much easier. Today, that AI is becoming an autonomous agent.&lt;/p&gt;

&lt;p&gt;In 36 months, everyone will have their own main AI agent. That agent will orchestrate a swarm of other specialized agents. People will no longer have to learn complicated dashboards. UI will become a thing of the past.&lt;/p&gt;

&lt;p&gt;It is not perfect yet. But we already have all the lego pieces required to replace massive amounts of software. Agents can learn skills. Agents can use memory. Agents can coordinate other tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Look at a CRM Example
&lt;/h2&gt;

&lt;p&gt;Data lives in a database. Let's assume your current CRM has an API. You want to stop paying 100 bucks a month for it. You ask your AI agent to migrate the CRM to something cheaper.&lt;/p&gt;

&lt;p&gt;The agent will analyze the transition. If it is simple, it will just migrate all the data to a new database. It will create thin wrappers as skills to access your data. It will create cron jobs to automate reports. It will build integrations via webhooks.&lt;/p&gt;

&lt;p&gt;You might be thinking about safety. What if the agent accidentally deletes the entire database? Who is responsible?&lt;/p&gt;

&lt;p&gt;Remember that AI agents will not perform raw SQL queries. Instead, they will use standard SDKs. They will build consistent and safe skills to perform predictable CRUD operations.&lt;/p&gt;

&lt;p&gt;But what if simple wrappers are not enough? What if you need complex roles, permissions, and advanced automations?&lt;/p&gt;

&lt;p&gt;In that case, the agent will simply spin up an instance of an open-source solution. Very soon, all popular open-source repos will have agent skills built directly into their codebase. Your agent will install a tool like Directus on your favorite hosting provider. It will set it up automatically. You will never even log into the admin panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 Ways to Survive
&lt;/h2&gt;

&lt;p&gt;If UI is dying, how do developers stay relevant? I see 6 safe zones for the upcoming years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Brand and Trust&lt;/strong&gt; &lt;br&gt;
AI will not replace personal connection. If you build a trustworthy brand, you can do masterminds, coaching, or live seminars. Human connection is safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fun and Leisure&lt;/strong&gt;&lt;br&gt;
People will have more free time. Cinema, sports, and immersive games will become super competitive. If you create something unique and engaging in this area, you are safe long-term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Heavy Compute and Hosting&lt;/strong&gt;&lt;br&gt;
AI agents will have to store data reliably somewhere. Hosting providers must be agent-friendly. They need to provide automatic backups, disaster recovery, and strong security. GPU clusters for rendering complex scenes are also safe. Agents cannot run heavy compute locally on a laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Bits to Atoms&lt;/strong&gt; &lt;br&gt;
Anything where the end product is physical will be safe. Custom t-shirt printing, 3D printing, and logistics are great examples. AI lives in the digital world. It needs you to touch the physical world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The Legal Wall&lt;/strong&gt;&lt;br&gt;
Payment processors like Stripe are safe. Anything that has massive compliance and legal requirements is a great moat. An AI agent cannot go to jail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything else is basically doomed.&lt;/strong&gt; Marketing automation, project management, and basic CRMs will take massive hits. Unless you build an agent infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Agent Infrastructure&lt;/strong&gt; &lt;br&gt;
This is the most important point for us developers. If you are building a SaaS, you must &lt;strong&gt;focus on the Agent Interface&lt;/strong&gt; instead of the User Interface.&lt;/p&gt;

&lt;p&gt;Create skills for agents so they can easily connect to your service. &lt;em&gt;No more user team slots or weird tiered memberships.&lt;/em&gt; Pay-as-you-go will become the new norm. &lt;/p&gt;

&lt;p&gt;Your API must be highly competitive. If it is too expensive, the AI will just replicate your core functionality using open-source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real World Example
&lt;/h2&gt;

&lt;p&gt;Let me give you a quick example of Agent Infrastructure.&lt;/p&gt;

&lt;p&gt;Today, you might use an AI model like Nano Banana Pro to generate an amazing image. You are super happy with it. But later you want to move the text or change the font. You are screwed because the font is hardcoded into the pixels.&lt;/p&gt;

&lt;p&gt;But imagine if a platform like Canva exposed a headless Agent Skill. Your agent could instruct Canva to compose the image in layers behind the scenes. Then, your agent could simply relocate the text and change the font. You would get the perfect result without ever visiting the Canva website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This already exists for videos.&lt;/strong&gt; Remotion released an AI skill that you can install right now. You can use it in Claude Code or Cursor. It composes a video for you. If you want changes, you just prompt it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Think of the future like a big company structure. You are the CEO. You have one main AI agent acting as your general manager. That manager controls specialized sub-agents. One takes care of your database. Another does internet research. A third one creates and schedules content.&lt;/p&gt;

&lt;p&gt;Digital transformation is already happening. The puzzle pieces are here. Ignoring this shift is like trying to make your horse faster during the Industrial Revolution while everyone else is already building a car.&lt;/p&gt;

&lt;p&gt;What are your thoughts on how the future will look? Are you building a UI wrapper or actual infrastructure? Let me know in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sass</category>
    </item>
    <item>
      <title>Building a TypeScript Video Editor as a Solo Dev</title>
      <dc:creator>Lukáš</dc:creator>
      <pubDate>Mon, 04 Nov 2024 23:23:07 +0000</pubDate>
      <link>https://forem.com/bladerik/building-a-typescript-video-editor-as-a-solo-dev-2oo8</link>
      <guid>https://forem.com/bladerik/building-a-typescript-video-editor-as-a-solo-dev-2oo8</guid>
      <description>&lt;p&gt;4 years after embarking on an exciting SaaS building journey, it's the right time to rebuild one of the key components of our app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple video editor for social media videos written in JavaScript.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the stack I decided to use for this rewrite, which is now a work in progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Svelte 5
&lt;/h2&gt;

&lt;p&gt;Since our frontend is written in &lt;code&gt;SvelteKit&lt;/code&gt;, this is the best option for our use case.&lt;/p&gt;

&lt;p&gt;The video editor is a separate private npm library I can simply add to our frontend. It's a headless library, so the video editor UI is completely isolated.&lt;/p&gt;

&lt;p&gt;The video editor library is responsible for syncing the video and audio elements with the timeline, rendering animations and transitions, rendering HTML texts into canvas, and much more.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SceneBuilderFactory&lt;/code&gt; takes in a scene JSON object as an argument to create a scene. &lt;code&gt;StateManager.svelte.ts&lt;/code&gt; then keeps the current state of the video editor in real time.&lt;/p&gt;

&lt;p&gt;This is super useful for drawing and updating the playhead position in the timeline, and much more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixi.js
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Pixi.js&lt;/code&gt; is an outstanding JavaScript canvas library.&lt;/p&gt;

&lt;p&gt;Initially, I started to build this project with &lt;code&gt;Pixi&lt;/code&gt; v8, but due to some reasons I'll mention later in this article, I decided to go with &lt;code&gt;Pixi&lt;/code&gt; v7.&lt;/p&gt;

&lt;p&gt;However, the video editor library is not tightly coupled to any dependencies, so it's easy to replace them if needed or to test different tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  GSAP
&lt;/h2&gt;

&lt;p&gt;For timeline management and complex animations, I decided to use &lt;code&gt;GSAP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's no other tool in the JavaScript ecosystem I'm aware of that allows building nested timelines, combined animations, or complex text animations in such a simple way.&lt;/p&gt;

&lt;p&gt;I have a GSAP business license, so I can also leverage additional tools to make more things simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Challenges
&lt;/h2&gt;

&lt;p&gt;Before we dive in into stuff I use in the backend, let's see some challenges you need to solve while building a video editor in javascript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synchronize video/audio with the timeline
&lt;/h3&gt;

&lt;p&gt;This question is often asked in the GSAP forum.&lt;/p&gt;

&lt;p&gt;It doesn't matter if you use GSAP for timeline management or not, what you need to do is a couple of things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On each render tick:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Get video relative time to the timeline. Let's say your video starts playing from the beginning at the 10-second mark of the timeline.&lt;/p&gt;

&lt;p&gt;Well, before 10 seconds you actually don't care about the video element, but as soon as it enters the timeline, you need to keep it in sync.&lt;/p&gt;

&lt;p&gt;You can do this by computing the relative time of the video, which must be computed from the video element's &lt;code&gt;currentTime&lt;/code&gt;, compared against current scene time and within an acceptable "lag" period.&lt;/p&gt;

&lt;p&gt;If the lag is larger than, let's say, 0.3 seconds, you need to auto-seek the video element to fix its sync with the main timeline. This also applies to audio elements as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other things you need to consider:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handle play / pause / ended states&lt;/li&gt;
&lt;li&gt;handle seeking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Play and pause are simple to implement. For seeking, I add the video seeking component id into our svelte StateManager, which will automatically change the state to "loading".&lt;/p&gt;

&lt;p&gt;StateManager has an EventManager dependency and on each state change, it automatically triggers a "changestate" event, so we can listen to these events without using &lt;code&gt;$effect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same thing happens after seeking is finished and the video is ready to play.&lt;/p&gt;

&lt;p&gt;This way we can show a loading indicator instead of play / pause button in our UI when some of the components are loading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text rendering is not as simple as you think
&lt;/h3&gt;

&lt;p&gt;CSS, GSAP, and GSAP's TextSplitter allow me to do really amazing stuff with text elements.&lt;/p&gt;

&lt;p&gt;Native canvas text elements are limited, and since the primary use case of our app is to create short-form videos for social media, they are not a good fit.&lt;/p&gt;

&lt;p&gt;Luckily, I found a way to render almost any HTML text into canvas, which is crutial for rendering the video output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pixi HTMLText&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This would have been the simplest solution; unfortunately, it did not work for me.&lt;/p&gt;

&lt;p&gt;When I was animating HTML text with GSAP, it was lagging significantly, and it also did not support many Google fonts I tried with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Satori&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Satori is amazing, and I can imagine it being used in some simpler use cases. Unfortunately, some GSAP animations change styles that are not compatible with Satori, which results in an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SVG with foreign object&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, I made a custom solution to solve this.&lt;/p&gt;

&lt;p&gt;The tricky part was supporting emojis and custom fonts, but I managed to solve that.&lt;/p&gt;

&lt;p&gt;I created an SVGGenerator class that has a generateSVG method, which produces an SVG like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"${width}"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"${height}"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 ${width} ${height}"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;${styleTag}&lt;span class="nt"&gt;&amp;lt;foreignObject&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/xhtml"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"transform-origin: 0 0;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;${html}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&amp;lt;/foreignObject&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The styleTag then looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;${&lt;/span&gt;&lt;span class="n"&gt;fontFamilyName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nt"&gt;src&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'${fontData}'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this to work, the HTML that we pass in needs to have the correct font-family set inside inline style. Font data needs to be a base64 encoded data string, something like data:font/ttf;base64,longboringstring&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Component Lifecycle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Composition over inheritance, they say.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As an exercise to get my hands dirty, I refactored from an inheritance-based approach to hook-based system.&lt;/p&gt;

&lt;p&gt;In my video editor, I call elements like &lt;code&gt;VIDEO&lt;/code&gt;, &lt;code&gt;AUDIO&lt;/code&gt;, &lt;code&gt;TEXT&lt;/code&gt;, &lt;code&gt;SUBTITLES&lt;/code&gt;, &lt;code&gt;IMAGE&lt;/code&gt;, &lt;code&gt;SHAPE&lt;/code&gt;, etc. &lt;code&gt;components&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before rewriting this, there was an abstract class &lt;code&gt;BaseComponent&lt;/code&gt;, and each component class was extending it, so &lt;code&gt;VideoComponent&lt;/code&gt; had logic for videos, etc.&lt;/p&gt;

&lt;p&gt;The problem was that it became a mess pretty quickly.&lt;/p&gt;

&lt;p&gt;Components were responsible for how they are rendered, how they manage their Pixi texture, how they are animated, and more.&lt;/p&gt;

&lt;p&gt;Now, there is only one component class, which is very simple.&lt;/p&gt;

&lt;p&gt;This now has four lifecycle events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;    &lt;span class="c1"&gt;// called on each render tick, video rewind, frame export...&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt;   &lt;span class="c1"&gt;// called when user changes component data in UI&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;destroy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5atagg6xhthrmnmfk5zd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5atagg6xhthrmnmfk5zd.png" alt="Component Lifecycle Code Sample" width="599" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This component class has a method called &lt;code&gt;addHook&lt;/code&gt; that changes its behavior.&lt;/p&gt;

&lt;p&gt;Hooks can hook into component lifecycle events and perform actions.&lt;/p&gt;

&lt;p&gt;For example, there is a &lt;code&gt;MediaHook&lt;/code&gt; that I use for video and audio components.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MediaHook&lt;/code&gt; creates the underlying audio or video element and automatically keeps it in sync with the main timeline.&lt;/p&gt;

&lt;p&gt;For building components, I used the builder pattern along with the director pattern (&lt;a href="https://refactoring.guru/design-patterns/builder" rel="noopener noreferrer"&gt;see reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This way, when building an audio component, I add &lt;code&gt;MediaHook&lt;/code&gt; to it, which I also add to video components. However, videos also need additional hooks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating the texture&lt;/li&gt;
&lt;li&gt;Setting up the sprite&lt;/li&gt;
&lt;li&gt;Setting the right location in the scene&lt;/li&gt;
&lt;li&gt;Handling rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach makes it very easy to change, extend, or modify the rendering logic or how the components behave in the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend and Rendering
&lt;/h2&gt;

&lt;p&gt;I tried multiple different approaches on how to render videos in the fastest and most cost-efficient ways.&lt;/p&gt;

&lt;p&gt;In 2020, I started with the simplest approach - rendering one frame after another, which is something that many tools do.&lt;/p&gt;

&lt;p&gt;After some trial-and-error, I switched to a rendering layers approach.&lt;/p&gt;

&lt;p&gt;That means our &lt;code&gt;SceneData&lt;/code&gt; document contains layers which contain components.&lt;/p&gt;

&lt;p&gt;Each of these layers is rendered separately and then combined with &lt;code&gt;ffmpeg&lt;/code&gt; to create the final output.&lt;/p&gt;

&lt;p&gt;The limitation was that a layer can only contain components of the same type.&lt;/p&gt;

&lt;p&gt;For example, a layer with video cannot contain text elements; it can only contain other videos.&lt;/p&gt;

&lt;p&gt;This obviously has some pros and cons.&lt;/p&gt;

&lt;p&gt;It was quite simple to render HTML texts with animations on Lambda independently and turn them into transparent videos, which were then combined with other chunks for the final output.&lt;/p&gt;

&lt;p&gt;On the other hand, layers with video components were simply processed with &lt;code&gt;ffmpeg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, this approach had a huge drawback.&lt;/p&gt;

&lt;p&gt;If I wanted to implement a keyframes system to scale, fade, or rotate the video, I would need to make ports of these features in &lt;code&gt;fluent-ffmpeg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is definitely possible, but with all the other responsibilities I have, I simply didn't make it.&lt;/p&gt;

&lt;p&gt;So I decided to go back to the first approach - rendering one frame after another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Express and BullMQ
&lt;/h2&gt;

&lt;p&gt;Rendering requests are sent to the backend server with &lt;code&gt;Express&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This route checks if the video isn't being rendered yet, and if not, it's added into the &lt;code&gt;BullMQ&lt;/code&gt; queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playwright / Puppeteer
&lt;/h2&gt;

&lt;p&gt;After the queue starts processing the render, it spawns multiple instances of headless Chrome.&lt;/p&gt;

&lt;p&gt;Note: this processing happens on a dedicated Hetzner server with AMD EPYC 7502P 32-Core Processor and 128 GB RAM, so it's quite a performant machine.&lt;/p&gt;

&lt;p&gt;Keep in mind Chromium doesn't have codecs, so I use &lt;code&gt;Playwright&lt;/code&gt; which makes it trivial to install Chrome.&lt;/p&gt;

&lt;p&gt;But still, the video frames came out black for some reason.&lt;/p&gt;

&lt;p&gt;I'm sure I was just missing something; however, I decided to split the video components into individual image frames and use these in the serverless browser instead of using videos.&lt;/p&gt;

&lt;p&gt;But still, the most important part was to avoid using the screenshot method.&lt;/p&gt;

&lt;p&gt;Since we have everything in one canvas, we can get it into an image with &lt;code&gt;.getDataURL()&lt;/code&gt; on the canvas, which is much faster.&lt;/p&gt;

&lt;p&gt;To make this simpler, I made a static page that bundles the video editor and adds some functions into &lt;code&gt;window&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is then loaded with &lt;code&gt;Playwright&lt;/code&gt;/&lt;code&gt;Puppeteer&lt;/code&gt;, and on each frame, I simply call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frameData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`window.setFrame(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;frameNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me the frame data that I can either save as an image or add into a buffer to render the video chunk.&lt;/p&gt;

&lt;p&gt;This whole process is split into 5-10 different workers, depending on the video length, which are merged into the final output.&lt;/p&gt;

&lt;p&gt;Instead of this, it can be offloaded to something like &lt;code&gt;Lambda&lt;/code&gt; as well, but I'm leaning towards using &lt;code&gt;RunPod&lt;/code&gt;. The only drawback of their serverless architecture is they use Python, which I'm not that familiar with.&lt;/p&gt;

&lt;p&gt;This way, the rendering might be split into multiple chunks that are processed on the cloud, and even rendering of a 60-minute video can be done in a minute or two. Nice to have, but that's not our primary goal or use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Did NOT Solve (Yet)
&lt;/h2&gt;

&lt;p&gt;The reason I downgraded from &lt;code&gt;Pixi&lt;/code&gt; 8 to &lt;code&gt;Pixi&lt;/code&gt; 7 is because &lt;code&gt;Pixi&lt;/code&gt; 7 also has the "legacy" version that supports 2D canvas. This is MUCH faster for rendering. A 60-second video takes around 80 seconds to render on the server, but if the canvas has WebGL or WebGPU context, I was able to render only 1-2 frames per second.&lt;/p&gt;

&lt;p&gt;Interestingly enough, serverless Chrome was much slower compared to headful Firefox when rendering WebGL canvases, according to my testing.&lt;/p&gt;

&lt;p&gt;Even using a dedicated GPU didn't help speed up the rendering by any significant margin. Either I was doing something wrong, or simply headless Chrome isn't very performant with WebGL.&lt;/p&gt;

&lt;p&gt;WebGL in our use case is great for transitions, which are usually quite short.&lt;/p&gt;

&lt;p&gt;One of the ways I plan to test regarding this is to render WebGL and non-WebGL chunks separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Components
&lt;/h2&gt;

&lt;p&gt;There are many parts involved in the project.&lt;/p&gt;

&lt;p&gt;Scene data is stored on &lt;code&gt;MongoDB&lt;/code&gt;, since the structure of the documents makes most sense to be stored in a schemaless database.&lt;/p&gt;

&lt;p&gt;The frontend, written in &lt;code&gt;SvelteKit&lt;/code&gt;, uses &lt;code&gt;urql&lt;/code&gt; as a GraphQL client.&lt;/p&gt;

&lt;p&gt;The GraphQL server uses PHP &lt;code&gt;Laravel&lt;/code&gt; with &lt;code&gt;MongoDB&lt;/code&gt; and the amazing &lt;code&gt;Lighthouse GraphQL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But this is a theme maybe for the next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;So that's it for now! There's a lot of work that needs to be done before putting this into production and replacing the current video editor, which is quite buggy and reminds me a bit of Frankenstein.&lt;/p&gt;

&lt;p&gt;Let me know what you think and keep on rockin'!&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
