<?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: Gtio</title>
    <description>The latest articles on Forem by Gtio (@gtoxlili).</description>
    <link>https://forem.com/gtoxlili</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%2F3879044%2F71fe7f47-f683-4016-aae0-83bfcd48a6bf.jpeg</url>
      <title>Forem: Gtio</title>
      <link>https://forem.com/gtoxlili</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gtoxlili"/>
    <language>en</language>
    <item>
      <title>I needed the stream URL when casting video — so I built a fake TV</title>
      <dc:creator>Gtio</dc:creator>
      <pubDate>Wed, 15 Apr 2026 09:48:24 +0000</pubDate>
      <link>https://forem.com/gtoxlili/i-needed-the-stream-url-when-casting-video-so-i-built-a-fake-tv-4ag4</link>
      <guid>https://forem.com/gtoxlili/i-needed-the-stream-url-when-casting-video-so-i-built-a-fake-tv-4ag4</guid>
      <description>&lt;p&gt;I wanted to grab the actual stream URL when casting a video from my phone to the TV. Not to watch it on the TV — I wanted the raw m3u8 link so I could record it with ffmpeg or play it in VLC on my laptop.&lt;/p&gt;

&lt;p&gt;Turns out, DLNA casting works by sending the media URL from the phone app to the TV via a standard SOAP request (&lt;code&gt;SetAVTransportURI&lt;/code&gt;). The TV then fetches and plays the stream on its own. The phone is just a remote control.&lt;/p&gt;

&lt;p&gt;So... what if your computer pretended to be a TV?&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;If you advertise a fake UPnP MediaRenderer on the local network, any app that supports DLNA casting (Bilibili, iQiyi, Youku, TikTok, and many more) will happily send you the real stream URL when you hit "cast."&lt;/p&gt;

&lt;p&gt;The app can't tell the difference — it's standard protocol, same as talking to a Samsung or Sony TV.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SSDP multicast&lt;/strong&gt; on &lt;code&gt;239.255.255.250:1900&lt;/code&gt; — announce ourselves as a MediaRenderer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UPnP device description&lt;/strong&gt; — reply with a minimal XML descriptor when queried&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOAP control&lt;/strong&gt; — the app sends &lt;code&gt;SetAVTransportURI&lt;/code&gt; with the actual stream URL. We extract it and we're done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing is ~500 lines of Python stdlib. No dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;wechat-finder-dlna

&lt;span class="c"&gt;# Print the captured URL&lt;/span&gt;
wechat-finder-dlna

&lt;span class="c"&gt;# Record directly with ffmpeg&lt;/span&gt;
wechat-finder-dlna &lt;span class="nt"&gt;--record&lt;/span&gt; stream.mp4 &lt;span class="nt"&gt;--duration&lt;/span&gt; 01:00:00

&lt;span class="c"&gt;# Pipe to VLC&lt;/span&gt;
wechat-finder-dlna | xargs vlc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use it as a library:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wechat_finder_dlna&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;capture&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Living Room TV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# url is the raw m3u8/mp4 link — do whatever you want with it
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phone and computer need to be on the same WiFi. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use an existing DLNA library?
&lt;/h2&gt;

&lt;p&gt;I looked at a few (like dlnap, macast), but they're full renderers — they actually play the video. I just wanted the URL. So I built the smallest possible MediaRenderer that only implements device discovery and &lt;code&gt;SetAVTransportURI&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Zero external dependencies. Pure stdlib (&lt;code&gt;http.server&lt;/code&gt;, &lt;code&gt;socket&lt;/code&gt;, &lt;code&gt;threading&lt;/code&gt;, &lt;code&gt;xml&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What apps work with this?
&lt;/h2&gt;

&lt;p&gt;Anything that supports DLNA/UPnP casting. I've tested with several Chinese streaming apps (they all use DLNA for casting), but it should work with any app that can cast to a smart TV on your network.&lt;/p&gt;

&lt;p&gt;The project is at &lt;a href="https://github.com/gtoxlili/wechat-finder-dlna" rel="noopener noreferrer"&gt;github.com/gtoxlili/wechat-finder-dlna&lt;/a&gt;. Feedback welcome — especially if you've tested it with apps I haven't tried.&lt;/p&gt;

</description>
      <category>python</category>
      <category>dlna</category>
      <category>networking</category>
      <category>streaming</category>
    </item>
    <item>
      <title>I needed resumable LLM streams in Go — so I built streamhub</title>
      <dc:creator>Gtio</dc:creator>
      <pubDate>Tue, 14 Apr 2026 17:10:55 +0000</pubDate>
      <link>https://forem.com/gtoxlili/i-needed-resumable-llm-streams-in-go-so-i-built-streamhub-349g</link>
      <guid>https://forem.com/gtoxlili/i-needed-resumable-llm-streams-in-go-so-i-built-streamhub-349g</guid>
      <description>&lt;p&gt;If you've built anything that streams LLM responses over SSE, you've probably hit this: the user refreshes the page, or their network blips, or the load balancer routes the reconnect to a different instance — and the stream is just gone. The generation keeps burning tokens on your backend, but the client sees nothing.&lt;/p&gt;

&lt;p&gt;In the JS/TS world this is mostly solved. Vercel shipped &lt;a href="https://github.com/vercel/resumable-stream" rel="noopener noreferrer"&gt;resumable-stream&lt;/a&gt;, there's &lt;a href="https://github.com/zirkelc/ai-resumable-stream" rel="noopener noreferrer"&gt;ai-resumable-stream&lt;/a&gt;, Ably has a whole &lt;a href="https://ably.com/blog/token-streaming-for-ai-ux" rel="noopener noreferrer"&gt;token streaming product&lt;/a&gt;. But if your backend is in Go? Nothing.&lt;/p&gt;

&lt;p&gt;I ran into this while working on a project where the LLM worker and the HTTP handler live in different processes. I needed something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persists chunks so reconnecting clients can replay what they missed&lt;/li&gt;
&lt;li&gt;delivers cancel signals across instances (user clicks "stop" on one node, generation stops on another)&lt;/li&gt;
&lt;li&gt;prevents duplicate producers (two requests racing to start the same session)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/gtoxlili/streamhub" rel="noopener noreferrer"&gt;streamhub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Two Redis primitives, that's it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Streams&lt;/strong&gt; store chunks. New subscribers read history first, then get live data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Pub/Sub&lt;/strong&gt; carries cancel signals. Fast, fire-and-forget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each producer gets a generation ID that acts as a fencing token — if a stale producer tries to write after losing ownership, the writes are rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code looks like
&lt;/h2&gt;

&lt;p&gt;Producer side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat:123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// called when someone cancels this session&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// another instance already owns this&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" world"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumer side (can be a completely different process):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat:123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// replays existing chunks first, then streams live&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flusher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flush&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;Cancel from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat:123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not just use X?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Just use Redis Streams directly"&lt;/strong&gt; — you can, but you'll end up reimplementing subscriber fan-out, replay-then-live handoff, generation fencing, and the cancel side-channel. That's what streamhub is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Use Centrifuge/Centrifugo"&lt;/strong&gt; — great project, but it's a full real-time messaging framework. If all you need is to make your LLM streams durable, it's a lot of surface area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Use vercel/resumable-stream"&lt;/strong&gt; — TypeScript only, tightly coupled to the Vercel AI SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status
&lt;/h2&gt;

&lt;p&gt;Early days. The API surface might still change. If you're dealing with this same problem in Go, I'd appreciate feedback: &lt;a href="https://github.com/gtoxlili/streamhub" rel="noopener noreferrer"&gt;github.com/gtoxlili/streamhub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>redis</category>
      <category>ai</category>
      <category>streaming</category>
    </item>
  </channel>
</rss>
