<?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: XiFin WU</title>
    <description>The latest articles on DEV Community by XiFin WU (@alittlebird).</description>
    <link>https://dev.to/alittlebird</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%2F2112627%2F94e0f563-93dd-4999-88b7-66b8b8ffe098.jpg</url>
      <title>DEV Community: XiFin WU</title>
      <link>https://dev.to/alittlebird</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alittlebird"/>
    <language>en</language>
    <item>
      <title>How to Stream a LiveKit Room to YouTube or Twitch via RTMP (Without Running FFmpeg)</title>
      <dc:creator>XiFin WU</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:36:02 +0000</pubDate>
      <link>https://dev.to/alittlebird/how-to-stream-a-livekit-room-to-youtube-or-twitch-via-rtmp-without-running-ffmpeg-3o2j</link>
      <guid>https://dev.to/alittlebird/how-to-stream-a-livekit-room-to-youtube-or-twitch-via-rtmp-without-running-ffmpeg-3o2j</guid>
      <description>&lt;p&gt;You have a &lt;a href="https://livekit.io/" rel="noopener noreferrer"&gt;LiveKit&lt;/a&gt; room — a multiplayer WebRTC session with several participants — and you want it on YouTube Live, Twitch, or any RTMP ingest. The catch: WebRTC and RTMP speak completely different protocols. You can't just "forward" the room; you have to &lt;strong&gt;composite the participants into one video&lt;/strong&gt; and &lt;strong&gt;mix their audio into one track&lt;/strong&gt;, then encode and push that single stream out over RTMP.&lt;/p&gt;

&lt;p&gt;There are two familiar paths. The DIY one is a GStreamer/FFmpeg box glued to your SFU — you own GPU encoding, layout, reconnects, and scaling forever. The platform-native one is &lt;a href="https://docs.livekit.io/home/egress/overview/" rel="noopener noreferrer"&gt;LiveKit Egress&lt;/a&gt;. There's a third path that's worth knowing about: a declarative, managed pipeline — you describe sources, mixers, and sinks in one JSON job, submit it to &lt;a href="https://avflow.dev" rel="noopener noreferrer"&gt;AVFlow&lt;/a&gt;, and the pipeline runs in the cloud.&lt;/p&gt;

&lt;p&gt;(Adapted from the official guide: &lt;a href="https://avflow.dev/guides/mix-livekit-to-rtmp" rel="noopener noreferrer"&gt;Mix LiveKit to RTMP&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious question: why not just LiveKit Egress?
&lt;/h2&gt;

&lt;p&gt;Fair question — if you're fully on LiveKit Cloud, Egress is the default answer and a good one. But its trade-offs show up quickly once you self-host, want more than one output, or step outside the LiveKit ecosystem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Egress self-hosted&lt;/th&gt;
&lt;th&gt;Egress on LiveKit Cloud&lt;/th&gt;
&lt;th&gt;AVFlow pipeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you operate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The egress service itself: composite workers rendering web templates, Redis, restarts, capacity&lt;/td&gt;
&lt;td&gt;Nothing — but LiveKit-only&lt;/td&gt;
&lt;td&gt;Nothing. An HTTP API; works with &lt;strong&gt;any&lt;/strong&gt; LiveKit deployment, cloud or self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Outputs per request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One request = one output. Streaming &lt;em&gt;and&lt;/em&gt; recording means separate egress instances doing the work twice&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;One JSON job fans out&lt;/strong&gt;: RTMP to multiple CDN &lt;code&gt;urls&lt;/code&gt; at once, or RTMP + HLS-to-S3 recording + more sinks in the same job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Beyond LiveKit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;The same pipeline also accepts Agora / Daily / Jitsi rooms, plus RTSP, SRT, WHEP, and file sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Preset picked at request start; deeper customization via web templates&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Grid / speaker / custom regions as plain config — and &lt;strong&gt;switchable mid-stream&lt;/strong&gt; via a job update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Pipeline nodes: e.g. ASR captions burned into the same stream, or translate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Billing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your infra + your ops time&lt;/td&gt;
&lt;td&gt;Per-minute on Cloud&lt;/td&gt;
&lt;td&gt;Per-minute of runtime, no monthly seats&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of those rows deserve a closer look:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fan-out in one job.&lt;/strong&gt; Egress is one output per request, so "stream to YouTube and record an HLS archive to S3" is two egress requests and two composites. In the pipeline model, one job wires the same mixers into both an &lt;code&gt;rtmp_push&lt;/code&gt; sink (with an array of CDN URLs) and a &lt;code&gt;segment&lt;/code&gt; sink for the archive. One composite, every output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No egress infrastructure, even for self-hosted LiveKit.&lt;/strong&gt; If you run your own LiveKit server, the Egress route means deploying and babysitting another stateful service. AVFlow never touches your deployment — it joins as a subscriber with a subscribe token, exactly like any other client. Your SFU stays untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's a pipeline, not a recorder.&lt;/strong&gt; Because the job is a DAG, you can insert nodes between the room and the output: wire an &lt;code&gt;asr&lt;/code&gt; node into the same RTMP stream and your YouTube broadcast carries live captions; add &lt;code&gt;translate&lt;/code&gt; for multilingual subtitles. And if you later move a project from LiveKit to Agora or Daily, the job shape stays the same — you swap the source type, not the architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layouts that change mid-broadcast.&lt;/strong&gt; Egress picks its layout preset when the request starts; changing it means tearing that egress down and starting another. A running AVFlow job is updatable in place: &lt;code&gt;POST /v1/jobs&lt;/code&gt; again with the same &lt;code&gt;name&lt;/code&gt;, and the diff applies &lt;strong&gt;without restarting the pipeline&lt;/strong&gt;. Switch the &lt;code&gt;video_mixer&lt;/code&gt; from &lt;code&gt;grid&lt;/code&gt; to &lt;code&gt;speaker&lt;/code&gt; the moment your host starts presenting, or wire a second room into the mix — while the RTMP stream stays up.&lt;/p&gt;

&lt;p&gt;The short version: &lt;strong&gt;Egress is the right default when you're all-in on LiveKit Cloud and need a single output. A managed pipeline wins when you want zero egress ops, one job that streams + records + captions simultaneously, or one API across multiple RTC platforms.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a room needs a mixer, not a relay
&lt;/h2&gt;

&lt;p&gt;An RTC room is many independent tracks: one video track per publisher, one audio track per speaker. An RTMP ingest expects exactly &lt;strong&gt;one&lt;/strong&gt; video stream and &lt;strong&gt;one&lt;/strong&gt; audio stream. So the pipeline must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscribe to every track in the room (a &lt;strong&gt;subscribe token&lt;/strong&gt;, not a publish token).&lt;/li&gt;
&lt;li&gt;Compose the videos into a single canvas — a grid layout is the common choice.&lt;/li&gt;
&lt;li&gt;Mix all audio into a single track.&lt;/li&gt;
&lt;li&gt;Encode H.264 + AAC and push to the RTMP endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skipping the mixers and wiring the room straight into an RTMP sink is rejected at submit time, not silently at runtime — which is much nicer to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Create a subscribe token
&lt;/h2&gt;

&lt;p&gt;Generate a token with &lt;strong&gt;subscribe&lt;/strong&gt; grants for the source room on the LiveKit project side. The &lt;a href="https://docs.livekit.io/home/server/generating-tokens/" rel="noopener noreferrer"&gt;LiveKit token docs&lt;/a&gt; cover this for every server SDK. You'll also want your RTMP ingest URL ready — for YouTube that looks like &lt;code&gt;rtmp://a.rtmp.youtube.com/live2/xxxx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Submit one JSON job
&lt;/h2&gt;

&lt;p&gt;A complete job looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"livekit-mix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"room_src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"livekit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"serverUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"wss://your-project.livekit.cloud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;subscribe-token&amp;gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"mix_video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"video_mixer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"room_src"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"canvas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"fps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"layout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"grid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"grid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"maxColumns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"mix_audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"audio_mixer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"room_src"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"sinks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"rtmp_out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"rtmp_push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mix_video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"mix_audio"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rtmp://a.rtmp.youtube.com/live2/xxxx"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"encoding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"videoCodec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"h264"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"audioCodec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"aac"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The graph is explicit.&lt;/strong&gt; &lt;code&gt;room_src&lt;/code&gt; feeds both mixers; the RTMP sink consumes &lt;em&gt;mixer&lt;/em&gt; outputs, not the raw source. This is what makes it a mix instead of a relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout is config, not code — and it isn't frozen at submit time.&lt;/strong&gt; A 1280×720@30 canvas with a grid capped at 3 columns; participants tile in automatically. Want 1080p? Change the canvas. Want a speaker layout mid-event? Re-submit the job with the same &lt;code&gt;name&lt;/code&gt;; the pipeline updates without a restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out is just more sinks.&lt;/strong&gt; Add a second URL to &lt;code&gt;urls&lt;/code&gt; for Twitch. Add a &lt;code&gt;segment&lt;/code&gt; sink with the same mixer inputs and the same job streams &lt;em&gt;and&lt;/em&gt; records. No duplicate composites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding lives on the sink.&lt;/strong&gt; H.264 + AAC is what YouTube and Twitch expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Submit it with one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST &lt;span class="s2"&gt;"https://api.avflow.dev/v1/jobs"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer ${AVFLOW_API_KEY}"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d @livekit-mix.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 — Monitor, then stop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# status&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.avflow.dev/v1/jobs/livekit-mix"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer ${AVFLOW_API_KEY}"&lt;/span&gt;

&lt;span class="c"&gt;# stop&lt;/span&gt;
curl -X DELETE &lt;span class="s2"&gt;"https://api.avflow.dev/v1/jobs/livekit-mix"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer ${AVFLOW_API_KEY}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for &lt;code&gt;status: "running"&lt;/code&gt;, then confirm the ingest on the YouTube/Twitch side. When the event ends, stop the job — billing is per-minute of runtime, not a monthly seat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this replaces
&lt;/h2&gt;

&lt;p&gt;The same job done by hand means: an SFU subscriber, an FFmpeg process with &lt;code&gt;xstack&lt;/code&gt; filters for grid layout, an audio amix graph, a hardware encoder you hope doesn't stall, and a supervisor to restart all of it. The self-hosted Egress route replaces FFmpeg but hands you a new service to run. The JSON above is the whole pipeline — stateless to run, trivially reproducible for the next event, and resizable by editing one number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full guide with cost breakdown: &lt;a href="https://avflow.dev/guides/mix-livekit-to-rtmp" rel="noopener noreferrer"&gt;Mix LiveKit to RTMP&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Grid recording to S3: &lt;a href="https://avflow.dev/guides/record-room-grid" rel="noopener noreferrer"&gt;Record a room&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://avflow.dev/nodes/video-mixer/" rel="noopener noreferrer"&gt;Video mixer node reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>livekit</category>
      <category>webrtc</category>
      <category>streaming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
