<?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: Illia Khorunzhyi</title>
    <description>The latest articles on DEV Community by Illia Khorunzhyi (@aggosh).</description>
    <link>https://dev.to/aggosh</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%2F4235303%2F04094eec-4707-4a2d-a22d-2d612a810636.jpg</url>
      <title>DEV Community: Illia Khorunzhyi</title>
      <link>https://dev.to/aggosh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aggosh"/>
    <language>en</language>
    <item>
      <title>One stalled RTMP destination froze all the others — ffmpeg tee, use_fifo, and a doubled backslash</title>
      <dc:creator>Illia Khorunzhyi</dc:creator>
      <pubDate>Mon, 09 Nov 2026 14:19:53 +0000</pubDate>
      <link>https://dev.to/aggosh/one-stalled-rtmp-destination-froze-all-the-others-ffmpeg-tee-usefifo-and-a-doubled-backslash-4ckf</link>
      <guid>https://dev.to/aggosh/one-stalled-rtmp-destination-froze-all-the-others-ffmpeg-tee-usefifo-and-a-doubled-backslash-4ckf</guid>
      <description>&lt;p&gt;We run 24/7 live streams from pre-recorded video: one playlist, sent to several platforms at once — YouTube, Twitch and Kick, often all three. Each stream is one FFmpeg process, and the fan-out goes through the &lt;code&gt;tee&lt;/code&gt; muxer.&lt;/p&gt;

&lt;p&gt;This post is about the failure that &lt;code&gt;onfail=ignore&lt;/code&gt; does not cover, how to reproduce it on your laptop in two minutes, and the one-character escaping mistake that takes every output down when you apply the fix.&lt;/p&gt;

&lt;p&gt;Everything below was measured on ffmpeg 7.1.5 in production and reproduced on 8.1 for this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;tee&lt;/code&gt; at all
&lt;/h2&gt;

&lt;p&gt;You can give ffmpeg several outputs instead:&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; &lt;span class="k"&gt;in&lt;/span&gt;.mp4 ... &lt;span class="nt"&gt;-f&lt;/span&gt; flv rtmp://a/... &lt;span class="nt"&gt;-f&lt;/span&gt; flv rtmp://b/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But then every output runs its own encoders. We copy the video and re-encode the audio to AAC, and with separate outputs the AAC encoder runs once per destination — it cost us &lt;strong&gt;1.9× the CPU&lt;/strong&gt;. &lt;code&gt;tee&lt;/code&gt; encodes once and hands the same packets to every muxer:&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;-re&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-map&lt;/span&gt; 0:v &lt;span class="nt"&gt;-map&lt;/span&gt; 0:a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="s2"&gt;"[f=flv:onfail=ignore]rtmp://a/app/KEY1|[f=flv:onfail=ignore]rtmp://b/app/KEY2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured with video copied and audio re-encoded, each extra destination costs about two points of a core: 9.7% for one, 11.9% for two, 13.0% for three.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;onfail=ignore&lt;/code&gt; handles a destination that fails
&lt;/h2&gt;

&lt;p&gt;The documented default for a tee slave is &lt;code&gt;onfail=abort&lt;/code&gt;: one output fails, the whole process stops. With &lt;code&gt;onfail=ignore&lt;/code&gt;, a destination that refuses the connection or drops it is removed, and ffmpeg tells you so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tee @ 0x…] Slave muxer #1 failed: Connection refused, continuing with 1/2 slaves.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set it explicitly even though our build already behaved that way without it. If you depend on a default the docs say is different, an ffmpeg upgrade can take every multi-destination stream off the air at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  …but not a destination that stops reading
&lt;/h2&gt;

&lt;p&gt;The bad case is a server that &lt;strong&gt;keeps the connection open and stops reading&lt;/strong&gt;. Nothing fails. &lt;code&gt;tee&lt;/code&gt; writes to every slave from one thread, so once the kernel's socket buffers to the stuck peer are full, that write blocks — and the healthy destinations stop getting packets too.&lt;/p&gt;

&lt;p&gt;There is no error, no "slave failed" line, and the process is alive. From the outside, every destination still looks connected, and none of them is receiving video.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reproduce it
&lt;/h3&gt;

&lt;p&gt;A TCP peer that reads for 8 seconds, then holds the socket open and goes quiet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stall_peer.py — read for N seconds, then keep the socket open and stop reading
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_for&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setsockopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOL_SOCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SO_REUSEADDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;read_for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;settimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then one healthy output (a local file) and one output to the stalling peer, in real time (&lt;code&gt;-re&lt;/code&gt;) for 70 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 stall_peer.py 19351 8 &amp;amp;
ffmpeg &lt;span class="nt"&gt;-re&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nv"&gt;testsrc2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1280x720:rate&lt;span class="o"&gt;=&lt;/span&gt;30 &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; sine &lt;span class="nt"&gt;-t&lt;/span&gt; 70 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; ultrafast &lt;span class="nt"&gt;-b&lt;/span&gt;:v 4M &lt;span class="nt"&gt;-g&lt;/span&gt; 60 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-map&lt;/span&gt; 0:v &lt;span class="nt"&gt;-map&lt;/span&gt; 1:a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-progress&lt;/span&gt; progress.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="s2"&gt;"[f=flv:onfail=ignore]healthy.flv|[f=flv:onfail=ignore]tcp://127.0.0.1:19351"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch &lt;code&gt;healthy.flv&lt;/code&gt; and &lt;code&gt;out_time&lt;/code&gt; in &lt;code&gt;progress.txt&lt;/code&gt;. Results, sampled every 10 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;slave options&lt;/th&gt;
&lt;th&gt;healthy output after 80 s&lt;/th&gt;
&lt;th&gt;stream time reached&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onfail=ignore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;frozen at 4.6 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;stuck at 8.5 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onfail=ignore:use_fifo=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;frozen at 4.8 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;stuck at 9.3 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onfail=ignore:use_fifo=1:fifo_options=queue_size=600\\:drop_pkts_on_overflow=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;34.9 MB, growing all the way&lt;/td&gt;
&lt;td&gt;68.8 s of 70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the first two runs, ffmpeg printed nothing until I killed the peer 80 seconds later; only then did &lt;code&gt;Slave muxer #1 failed: Broken pipe&lt;/code&gt; appear. Bigger buffers only delay the freeze: in our first test of this, the stream ran 51.7 seconds before stopping dead. It still stopped.&lt;/p&gt;

&lt;p&gt;Note the middle row: &lt;strong&gt;&lt;code&gt;use_fifo=1&lt;/code&gt; on its own does not help.&lt;/strong&gt; It moves each slave into its own thread with its own queue, but the queue blocks when it is full, so you've only added a buffer before the same stall. The queue has to be allowed to drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;use_fifo&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;:&lt;span class="n"&gt;fifo_options&lt;/span&gt;=&lt;span class="n"&gt;queue_size&lt;/span&gt;=&lt;span class="m"&gt;600&lt;/span&gt;\\:&lt;span class="n"&gt;drop_pkts_on_overflow&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each destination gets its own thread and a queue of 600 packets — roughly ten seconds of a 1080p stream, long enough to ride out a hiccup and small enough to afford per destination on a box running twenty-odd streams. When a destination stops reading, its queue fills and starts dropping packets; the other destinations never notice.&lt;/p&gt;

&lt;p&gt;The stalled destination gets a stream with holes in it, which is the right trade when the alternative is all of them going dark. With &lt;strong&gt;one&lt;/strong&gt; destination we don't use any of this: there is nothing to protect, and blocking is better there — the stream stalls, our health check notices the frozen clock and restarts it cleanly, instead of feeding a platform a broken stream it may not recover from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The doubled backslash
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fifo_options&lt;/code&gt; is a dictionary &lt;em&gt;inside&lt;/em&gt; the slave's option list, and both use &lt;code&gt;:&lt;/code&gt; as a separator. So the inner &lt;code&gt;:&lt;/code&gt; has to be escaped — and &lt;code&gt;tee&lt;/code&gt; unescapes the slave options once, and the nested dictionary once more. One backslash is not 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;# single backslash&lt;/span&gt;
&lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="s1"&gt;'[f=flv:onfail=ignore:use_fifo=1:fifo_options=queue_size=600\:drop_pkts_on_overflow=1]a.flv|…'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[flv @ 0x…] Unknown option 'drop_pkts_on_overflow'
[tee @ 0x…] Slave muxer #0 failed: Invalid argument, continuing with 1/2 slaves.
[tee @ 0x…] All tee outputs failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 234, and both files stop at 383 bytes — the header. The single-escaped &lt;code&gt;drop_pkts_on_overflow&lt;/code&gt; escapes the fifo dictionary and gets handed to the flv muxer, which rejects it, and &lt;code&gt;onfail=ignore&lt;/code&gt; cannot save you because &lt;em&gt;every&lt;/em&gt; slave carries the same bad option. With &lt;code&gt;\\&lt;/code&gt; the same command writes both files to the end.&lt;/p&gt;

&lt;p&gt;In Python, build the argument as a raw string and pass it to the process without a shell, so nothing else gets a chance to eat a backslash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;FLV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f=flv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flvflags=+no_duration_filesize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;onfail=ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;FIFO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;use_fifo=1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fifo_options=queue_size=600\\:drop_pkts_on_overflow=1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tee_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FLV&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIFO&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# asyncio.create_subprocess_exec("ffmpeg", ..., "-f", "tee", tee_spec(urls))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A user-supplied URL is part of the tee syntax
&lt;/h2&gt;

&lt;p&gt;The whole spec is one string shaped &lt;code&gt;[opts]url1|[opts]url2&lt;/code&gt;. A &lt;code&gt;|&lt;/code&gt; inside a URL starts another slave:&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;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; testsrc &lt;span class="nt"&gt;-t&lt;/span&gt; 1 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-map&lt;/span&gt; 0:v &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="s1"&gt;'[f=flv:onfail=ignore]x1.flv|x2.flv'&lt;/span&gt;
&lt;span class="c"&gt;# writes x1.flv AND x2.flv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your users paste their own ingest URLs, that is a way to push to a destination you never counted or reported on. Reject &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;, &lt;code&gt;\&lt;/code&gt; and whitespace in destination URLs before they reach the spec — and accept only &lt;code&gt;rtmp://&lt;/code&gt; and &lt;code&gt;rtmps://&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing which destination died
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tee&lt;/code&gt; gives no per-destination byte counts (&lt;code&gt;total_size=N/A&lt;/code&gt; in &lt;code&gt;-progress&lt;/code&gt;), so the log lines are the signal. These are the ones worth parsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tee @ 0x…] Slave muxer #1 failed: Connection refused, continuing with 1/2 slaves.
[fifo @ 0x…] Error opening rtmp://…: Input/output error
[tee @ 0x…] All tee outputs failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mask the stream key before you log any of them: the address in these lines contains it.&lt;/p&gt;




&lt;p&gt;I run &lt;a href="https://theloops.live" rel="noopener noreferrer"&gt;TheLoops&lt;/a&gt;, a service that streams pre-recorded video 24/7 to YouTube, Twitch and Kick from the cloud. Every multi-destination stream it runs goes through the spec above. Questions about ffmpeg fan-out are welcome in the comments.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>streaming</category>
      <category>video</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
