I Just Wanted Two TV Channels. Somehow, I Built an IPTV Headend. 😅
Two in-house channels, two Linux VMs, a lot of FFmpeg, and one Android emulator that briefly decided it owned all the RAM.
Like most of my little pet projects, this one started with a random thought: what if I could build two in-house TV channels using our existing infrastructure, some open-source tools, and absolutely no fancy broadcasting hardware?
Seemed like a fun little experiment. Besides, how complicated could two TV channels possibly be? 😅
The idea was simple.
One channel would continuously loop promotional videos: experiences, offers, events, and all the usual things we'd want guests to discover. The other would carry live events from allegedly "approved" online sources ✌️, whenever the occasion called for it.
That's it. Two channels. Not a television network. Not a Netflix competitor. Just two little additions to the existing IPTV lineup.
And naturally, what started as a harmless little experiment somehow turned into two applications, an Android TV emulator, and a rather intimate relationship with FFmpeg. 😂
1. The requirement was simple. The price tags weren't.
There are commercial products for all of this: broadcast encoders, IPTV middleware, dedicated playout boxes, and licensing plans with impressive enthusiasm for recurring payments.
Those solutions make sense in the right environment. But we already had a network, an IPTV distribution system, and virtual-machine infrastructure. Buying more hardware and licenses for just 2 channels felt like bringing a forklift to move a chair.
So I asked a dangerous question: What if we just did it with software?
2. First, a little multicast magic
If 50 televisions watch a normal unicast stream, the server may send 50 separate copies. With multicast, the source sends one stream to a group address, and the network delivers it to interested receivers.
That group address isn't a conventional server IP. It's a multicast destination. Receivers subscribe using IGMP, while switches with IGMP snooping help avoid flooding the stream everywhere. Depending on the network, an IGMP querier and multicast routing may also be needed.
Our existing IPTV network already spoke this language. We just needed to give it something to watch.
3. FFmpeg became my tiny broadcast station
Enter FFmpeg: the open-source multimedia Swiss Army knife that can encode, decode, resize, repackage, and stream video and occasionally respond with an error message that reads like a personal challenge.
The idea was straightforward: read a promotional video, loop it, convert it into a television-friendly format, wrap it in MPEG Transport Stream (MPEG-TS), and send it over UDP multicast.
Why all those formats? A codec determines how audio or video is compressed; MPEG-TS is the container that carries the streams; UDP is how the packets travel. They solve different problems, and older televisions can be surprisingly picky about the combination.
Here's a simplified example to experiment with on a multicast-enabled test network:
ffmpeg -re -stream_loop -1 -i promo.mp4 \
-vf 'fps=25,scale=1280:720,format=yuv420p' \
-c:v mpeg2video -b:v 2500k \
-c:a mp2 -b:a 128k \
-f mpegts 'udp://239.80.0.1:1234?pkt_size=1316&ttl=1'
In a multi-interface server, you'd also choose the correct outbound interface. And yes, a test stream should stay on a network where you're allowed to send multicast traffic.
4. One terminal command became a web app
The first stream was promising. But I didn't fancy running FFmpeg commands by hand every time someone wanted to change a video or move a channel.
So I built Multicast Studio: a self-hosted control center with Vue 3, TypeScript, and Tailwind CSS for the interface, Node.js and Hono for the API, and FFmpeg doing the actual heavy lifting.
It handles channel creation, media playout, live inputs, status monitoring, and multicast destinations. We even added channel swapping, because stopping, deleting, and recreating channels just to exchange their multicast addresses gets old very quickly.
At this point, channel one was behaving. Channel two had other ideas.
5. The internet is full of streams. Working ones? Different story.
For live events, I started looking at IPTV-org, a community-maintained directory of publicly listed IPTV stream links and playlists. It's a useful discovery resource, but it doesn't host or guarantee the streams.
A link might work today and fail tomorrow. Some require particular regions or players. Some simply disappear. Finding a usable, authorized source could become its own little treasure hunt, minus the treasure.
Then I came across Android TV apps that could play sources more conveniently. Instead of buying another physical Android box, I wondered: Could I run Android TV inside a VM and treat its screen as a live video source?
You can probably guess what happened next.
6. So I virtualized a television. Obviously. 😂
I created an Android TV emulator on Ubuntu and built Android Emulator Manager so I could start it, control it with a browser based remote, check its health, and manage streaming without living in an SSH terminal.
Under the hood, ADB talks to Android, scrcpy captures the emulator's screen and audio, and FFmpeg converts that capture into an HTTP MPEG-TS feed. Multicast Studio then ingests that feed and broadcasts it to the IPTV network.
A FIFO is a named pipe: one process writes data while another reads it, without needing a growing video file on disk. The HTTP endpoint connects the two VMs. It worked—but the first version of that FFmpeg HTTP server accepted only one viewer at a time, and disconnecting could trigger a broken pipe. We had to make stream exits and restarts more graceful.
7. VLC said yes. The actual television said no.
This was my favorite debugging lesson: playing perfectly in VLC does not mean a stream is television-compatible.
Our older LG hospitality TVs stuttered and froze on the emulator-fed channel, even while VLC played the same multicast stream. The culprit wasn't simply “not enough bitrate.” We were seeing irregular audio timestamps; FFmpeg's delightful non-monotonic DTS warnings, alongside a format that needed more predictable timing.
The fix was to normalize video frame timing, resample audio timestamps, and transcode into the MPEG-2/MP2 combination our receivers handled reliably. That costs CPU, but it made the broadcast far more predictable.
Then, just as the picture settled down, Linux contributed to the story.
8. And then the emulator ate the RAM
Our Android guest was configured with 4 GB of memory. Its QEMU host process, however, could use considerably more once emulator overhead was included. Eventually the Ubuntu VM ran short of memory, and Linux's out-of-memory (OOM) killer terminated the emulator 😓.
Subtle? Not especially.
We added resource monitoring, startup memory checks, configurable emulator sizing, and protective shutdown behavior to Android Emulator Manager. The lesson was simple: a virtual machine still needs real memory, and a guest's RAM setting isn't the same as its total host footprint.
9. Want to build something similar?
You don't need the exact same resort setup. A multicast-capable test network, a Linux host, and a compatible receiver are enough to explore the idea.
- Start with the network. Confirm multicast is supported, IGMP is configured appropriately, and your test receivers can join the group.
- Try a local video first. Install FFmpeg and use a short looping file before introducing live internet feeds.
- Set up Multicast Studio. Follow the repository README to configure the API, web interface, media, and multicast output.
- Add Android only if you need it. Follow Android Emulator Manager to install the Android SDK/emulator, ADB, scrcpy, and FFmpeg, then connect its live feed to Multicast Studio.
A practical note: use sources you have permission to distribute. Being able to play a stream isn't the same as having rights to rebroadcast it, especially for sports and other live events.
10. Two channels, quite a few lessons
I set out to avoid unnecessary infrastructure and licensing costs. I ended up learning far more than I expected about transport streams, codecs, multicast networking, Android virtualization, and the very personal relationship between FFmpeg and timestamps.
The best part? Both tools are open source, and the setup can be adapted to different environments rather than tied to one property's infrastructure.
Would I call it overengineering? Maybe a little.
Would I do it again? Absolutely. 😄
The projects: Multicast Studio · Android Emulator Manager
