< the skull

BRIDGING ERSATZTV AND OWNCAST
=============================
2026-08-14

I haven’t been building anything lately, just maintaining what I already have.
Watching, taking notes, and thinking.

But I’ll leave a small note about something I built in the past and have been
“refining” right now, which was basically a channel using Owncast. This little
adventure taught me how to understand two pieces of software: Owncast itself,
and the other one was ErsatzTV.

I had always wanted to learn how to use ErsatzTV, but until then I never really
had a reason to. And I have to admit, the only reason I hadn’t tried it before
was because the latest news I had read said that the developer had abandoned the
project. Apparently not after all...

I had already written before about how I exposed my CGNAT to the internet in an
extremely functional way, which allowed me to host several applications
cohesively without depending on Cloudflare. In fact, maybe someday I’ll write
more about it and explain this architecture that I’m so proud of in greater
detail.

My problem now was figuring out how to run a 24/7 channel (at least whenever I
wanted) while keeping it lightweight on the internet. Hosted on Unraid.

On Unraid, I already had Owncast and ErsatzTV properly configured, but something
was missing in between, something to push the stream. So I created a
Frankenstein image that I called Owncast-Feed.

Why it exists
-------------

I have two pieces that don’t speak the same language:

ErsatzTV — creates a 24/7 “TV channel” (channel 1 = MadArabTV) and makes it
available as HLS (`.m3u8`), which is a playback format (the player pulls the
stream).

Owncast — is mine public streaming platform, but it only accepts input through
RTMP, an ingest format where something pushes video into it, just like OBS does
with Twitch/YouTube.

What’s missing is a bridge between the two: something that pulls the HLS stream
from ErsatzTV and continuously pushes it as RTMP to Owncast. That “something” is
owncast-feed: a container that does nothing but run a single ffmpeg process
performing this stream conversion, with no interface, 24 hours a day while it’s
running.

how it works

How it works (the ffmpeg)
-------------------------

The core is a single ffmpeg command that:

Reads the HLS stream from ErsatzTV (`-i
http://192.168.50.7:8409/iptv/channel/1.m3u8`), using reconnection flags
(`-reconnect ...`) to survive network fluctuations.

Normalizes the video using filters: `fps=30` (forces a constant frame rate),
`scale/pad` to a fixed 1920×1080 resolution (adding black bars when necessary).

Re-encodes the video as H.264 on the GPU (`-c:v h264_vaapi`) at 3000 kbps, and
the audio as AAC with `aresample=async` (which corrects audio drift).

Pushes the result as FLV/RTMP to Owncast (`-f flv
rtmp://192.168.50.7:1935/live/<streamkey>`).

The key decision: why re-encode instead of using copy?
------------------------------------------------------

This is the most important part of the design. Using `-c copy` (passing the
stream through without re-encoding) would use almost no CPU, but it would break
everything.

ErsatzTV switches programs all the time, and every switch creates timestamp
discontinuities in the HLS stream. If those discontinuities were passed directly
into RTMP, Owncast would drop every viewer whenever the channel changed videos.

Re-encoding with `fps=30` (CFR) + `aresample` “stitches” those transitions
together into a single, continuous stream with no gaps.

That’s why re-encoding is intentional, it’s what ensures that nobody gets
disconnected from Owncast when the channel changes videos.

And I think, once again, that’s all...


< the skull