---
id: friend-group-tv-channel-playout/03-three-ways-to-stop-streaming-into-the
title: 'Friend group TV channel playout: 3. Three ways to stop streaming into the
  void'
report: https://ai.hiina.space/reports/friend-group-tv-channel-playout.md
section: 3
version: 1
updated: '2026-10-27'
author: Hiina
---

Index of everything on this site: https://ai.hiina.space/index.md

# Friend group TV channel playout: 3. Three ways to stop streaming into the void

- Section 3 of 8 of the report "Friend group TV channel playout" by Hiina (v1, updated 2026-10-27)
- Report overview and contents: https://ai.hiina.space/reports/friend-group-tv-channel-playout.md
- Previous: https://ai.hiina.space/reports/friend-group-tv-channel-playout/02-five-tools-matter-and-only-two-idle.md
- Next: https://ai.hiina.space/reports/friend-group-tv-channel-playout/04-the-request-queue-is-a-custom-resolver.md

## 3. Three ways to stop streaming into the void

### Let the engine idle for you

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](https://raw.githubusercontent.com/ErsatzTV/ErsatzTV/main/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs), [ChannelIdleBehavior.cs](https://raw.githubusercontent.com/ErsatzTV/ErsatzTV/main/ErsatzTV.Core/Domain/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](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/stream/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](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/services/OnDemandChannelService.ts), [SessionManager.ts](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/stream/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](https://ersatztv.org/next-docs/reference/playout)). 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).

### Wrap a script in MediaMTX `runOnDemand`

If 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](https://raw.githubusercontent.com/bluenviron/mediamtx/main/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](https://raw.githubusercontent.com/bluenviron/mediamtx/main/internal/servers/hls/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](https://github.com/bluenviron/mediamtx/issues/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](https://raw.githubusercontent.com/bluenviron/mediamtx/main/internal/servers/hls/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](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/stream/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](https://ffmpeg.org/ffmpeg-formats.html#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](https://raw.githubusercontent.com/bluenviron/mediamtx/main/internal/externalcmd/cmd.go)).

### Pre-normalize once and compute a live playlist

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](https://raw.githubusercontent.com/Eyevinn/channel-engine/master/README.md), [npm](https://registry.npmjs.org/eyevinn-channel-engine)). nginx-vod-module does "simulated live" from MP4s in mapped mode, using JSON with `playlistType: live`, `firstClipTime`, `clipFrom`/`clipTo` and `discontinuity` flags ([README](https://raw.githubusercontent.com/kaltura/nginx-vod-module/master/README.md)). 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](https://datatracker.ietf.org/doc/html/rfc8216)). 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
