Index of everything on this site: https://ai.hiina.space/index.md
ErsatzTV and Tunarr both share one ffmpeg session per channel among
all viewers, start it lazily, and decide when to stop by how recently
anyone requested the playlist. HLS has no persistent connection, so
request recency is the only signal any of them has. In the ErsatzTV app,
ffmpeg.segmenter.timeout_seconds defaults to 1
minute, and a per-channel ChannelIdleBehavior enum
offers StopOnDisconnect or KeepRunning (StartFFmpegSessionHandler.cs,
ChannelIdleBehavior.cs).
Standalone Next uses the 90 s heartbeat described above.
Tunarr adds a feature that suits a friend group: an on-demand
channel mode. Normally a Tunarr lineup runs on wall-clock time,
so a returning viewer lands wherever the schedule has reached (StreamProgramCalculator.ts).
With on-demand enabled (per channel,
onDemand: { enabled }), Tunarr pauses the channel as soon
as the last real client disconnects. It saves a cursor,
cursor + (pauseTime - lastResumed) - rewindMs, and resumes
from that point next time, so nobody misses episode 3 because nobody was
watching (OnDemandChannelService.ts,
SessionManager.ts).
The source has one trap. If the lineup changed after the last resume,
the cursor resets to zero and the channel starts again from the
beginning. I found no user-facing docs page for this mode.
Next works differently. Its playout items carry absolute RFC 3339 start and finish times, so a scheduled block plays at its wall-clock time whether or not anyone is watching (playout reference). For a "cartoon hour at 9 pm" channel, that is what you want. For a "resume where we left off" channel, Tunarr's behavior comes through the queue instead (see the queue section).
runOnDemandIf you keep MediaMTX as the front end, runOnDemand runs
a command when a reader requests a path that has no publisher, and sends
it SIGINT runOnDemandCloseAfter (default 10
s) after the last reader leaves. Readers wait up to
runOnDemandStartTimeout (default 10 s) for
the command to start publishing. The command receives
MTX_PATH and MTX_QUERY in its environment (mediamtx.yml).
The HLS caveat is that the HLS muxer itself registers as a reader of the
path. It goes away only after hlsMuxerCloseAfter (default
60 s) with no HTTP requests (muxer.go).
The maintainer confirms that with HLS-only viewers the stop always takes
about a minute, and recommends WebRTC for exact reader counting (issue
#3903). Plan on about 70 s of wasted work after the last tab
closes. You can lower hlsMuxerCloseAfter to 15 to
20 s, at the risk of dropping a viewer whose player stalls for a moment.
Leave hlsAlwaysRemux off. The source exempts only
sourceOnDemand paths from it, so an always-on muxer would
plausibly act as a permanent reader of a runOnDemand path
and keep the command running (server.go).
That last point is my inference from the source, not something I
tested.
The script follows the "schedule as timeline" pattern. Define the
schedule as a deterministic list of items with durations and an epoch.
At startup, compute the current item and offset. Tunarr's version is
about 60 lines: elapsed time modulo channel duration, a binary search
over precomputed start offsets, and a skip to the next item when less
than about a second of the current one remains (StreamProgramCalculator.ts).
Then exec a single long-running ffmpeg that reads an
ffconcat list, with an inpoint on the first entry, and
publishes with -re to
rtsp://localhost:$RTSP_PORT/$MTX_PATH. The concat demuxer
requires every file to share codecs and timebase, and its docs warn that
inpoint is imprecise on inter-frame codecs (FFmpeg
concat).
My recommendation, which is inference rather than anything measured,
is to normalize the whole library once at ingest: one
resolution, constant frame rate, fixed keyframe interval, H.264 and AAC
at 48 kHz stereo. The on-demand ffmpeg can then run
-c copy. The "encoding into the void" during those idle 70
s becomes a remux costing almost nothing. With anime OPs/EDs at around
90 s and commercials at 15 to 30 s, avoid restarting ffmpeg for each
clip. Each republish leaves a visible gap, and with
runOnDemandRestart set, MediaMTX waits 5 s
before restarting a command that exited (cmd.go).
The zero-CPU option skips transcoding at view time altogether. Encode
every clip once into HLS segments with identical parameters. A small
service then emits a sliding-window live m3u8 whose position comes from
the schedule and the clock. When nobody watches, only a static file
server is running. Eyevinn Channel Engine (Apache-2.0, 5.1.3) builds
24/7 live HLS channels from VOD HLS. You plug in an
AssetManager whose getNextVod() returns the
next asset, which also works as a queue hook (README,
npm).
nginx-vod-module does "simulated live" from MP4s in mapped mode, using
JSON with playlistType: live, firstClipTime,
clipFrom/clipTo and discontinuity
flags (README).
For a handful of friends, a hand-rolled generator of about 150 lines is
probably lighter than either (my estimate). It has to follow RFC 8216's
live-playlist rules: EXT-X-DISCONTINUITY at every item
boundary, a constant TARGETDURATION, and a
MEDIA-SEQUENCE that only increases, which in practice means
a persisted segment counter that survives restarts (RFC 8216).
Joining costs one playlist fetch plus player buffering, roughly 6 to 10
s with 2 s segments (estimate). The tradeoffs are disk space for a
second copy of the library, and losing per-channel overlays and
on-the-fly graphics. I could not confirm whether Channel Engine does any
work for a channel nobody is watching.
Text licensed CC BY 4.0 (attribution: Hiina, ai.hiina.space). Source: https://github.com/hiinaspace/ai-hiina-space