---
id: friend-group-tv-channel-playout
title: Friend group TV channel playout
version: 1
published: '2026-10-27'
updated: '2026-10-27'
author: Hiina
produced-by: Claude deep-research run with citation checking
model: claude-opus-5-5
sources-through: '2026-10-26'
status: current
takeaway: For a small friend group's pseudo-TV channel, run ErsatzTV Next standalone
  behind a small custom scheduler and request-queue resolver; it idles when unwatched.
  For audio only, use AzuraCast 0.24.0.
topics:
- streaming
- media-playout
source-sha256: f68068e1c91ad9261fd529de170026de563d43fa41cbdc80a0491fbdfbdd791d
---

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

# Friend group TV channel playout

- Author: Hiina (hiina@hiina.space)
- Produced by: Claude deep-research run with citation checking
- Version: 1 · published 2026-10-27 · updated 2026-10-27 · status: current
- Canonical: https://ai.hiina.space/reports/friend-group-tv-channel-playout.md (this version as one file: https://ai.hiina.space/reports/friend-group-tv-channel-playout/v1.md)
- Takeaway: For a small friend group's pseudo-TV channel, run ErsatzTV Next standalone behind a small custom scheduler and request-queue resolver; it idles when unwatched. For audio only, use AzuraCast 0.24.0.
- Topics: streaming, media-playout
- Model: claude-opus-5-5
- Sources through: 2026-10-26

What is the best way to run a shared, always-available TV channel for a small friend group, on top of an existing self-hosted HLS streaming setup built on MediaMTX? The channel should mix scheduled anime episodes with filler (openings and endings, old commercials, running-gag clips), let friends request what plays next, and not burn CPU encoding video when nobody is watching. This report compares the open-source playout tools as of late October 2026 on scheduling, request queues, idle behaviour and browser output, and sketches the small glue service that every option still needs.

## 1. Summary

The best fit in October 2026 is **ErsatzTV Next running standalone, fed by a small custom scheduler and queue service**. Next is a Rust streaming engine. It takes a JSON playout, serves each channel as HLS that a browser can play, starts a channel only when someone requests its playlist, and stops it after **90 seconds without a client**. Its `dynamic` playout item asks an external HTTP endpoint "what plays now?" at playback time, which is the only documented request-queue hook among the video tools. For a no-code option, use **Tunarr with on-demand mode**. It pauses the channel's position when nobody watches, but it has no request queue. For a setup that would rather stay on an existing MediaMTX pipeline, a **`runOnDemand` wrapper script over a pre-normalized library** gets you most of the way with roughly 70 seconds of wasted remuxing after the last viewer leaves. The cheapest option of all serves a computed live m3u8 from pre-segmented files, so nothing runs at view time. No project ships a finished "community TV with request queue and auto-fill". The glue in every option is the same small piece: a queue store, a Discord or web front end, and a resolver that picks the next clip. For audio only, **AzuraCast 0.24.0** (released 25 Oct 2026) now has request-queue playlists and does the whole job out of the box.

## 2. Five tools matter, and only two idle natively

The table compares the main candidates. Versions and maintenance status come from the GitHub releases API and in-repo changelogs as of 26 Oct 2026. The prose sections below give the sources.

| Tool | Kind | Scheduling model | Request / insert-now | Idle when unwatched | Browser output / MediaMTX fit | Status, Oct 2026 |
|---|---|---|---|---|---|---|
| ErsatzTV Next (standalone) | Streaming engine, Rust, MIT | External JSON playout with absolute start/finish per item; scheduling is out of scope | `dynamic` item resolved over HTTP at playback time | Yes: lazy start on first playlist request, stops after 90 s without heartbeat | Serves HLS itself on port 8409; reverse-proxy it, MediaMTX not needed for video | v0.2.3, 23 Oct; pre-1.0 |
| ErsatzTV app (formerly "Legacy") | Library + scheduler app, Zlib | Classic, block, YAML sequential, Python-scripted schedules | No insert-now endpoint found | HLS segmenter: stop after 1 min idle (default), per-channel KeepRunning | HLS m3u8 | v26.11.5, 24 Oct; old streaming engine removed in 26.12, Next engine becomes the only one |
| Tunarr | Library + scheduler app, Zlib | Time slots, random slots, filler lists; lineup loops from channel start | None; lineup is replaced wholesale | Opt-in per-channel on-demand mode pauses the cursor | HLS, browser streaming | v2026.10.4, 25 Oct |
| ffplayout | 24/7 broadcast playout, Rust, GPL-3.0 | One JSON playlist per day | REST API rewrites the playlist; edits only past the next-but-one clip | No; continuous process | Writes HLS files or pushes RTMP into MediaMTX | 2.6.0 last plain stable; 2.7.0-rc.1, 24 Oct |
| Liquidsoap | Stream scripting language | Anything you script | `request.queue` | Only the documented audio relay pattern | `output.file.hls` (video OK), `output.url` to RTMP | 2.4.6, 8 Oct |
| MediaMTX `runOnDemand` + script | DIY wrapper on an existing MediaMTX setup | Your function `now -> (item, offset)` | Whatever your script reads | Yes, about 60 s + 10 s after last HLS request | Native | MediaMTX v1.21.3, 25 Oct |
| Computed m3u8 (Channel Engine, nginx-vod-module, hand-rolled) | Playlist generator over pre-encoded media | Your schedule | Append to the timeline | Nothing to idle; just a web server | Bypasses MediaMTX; static HTTP | Channel Engine 5.1.3, May 2026, dependency bumps through Oct |
| OpenTogetherTube | Watch-party app | Shared queue, vote or DJ mode | Native | Nothing encodes; viewers' browsers play files | Plays HTTP MP4 and HLS/DASH VOD, no live | Commits through 22 Oct |
| AzuraCast (audio) | Turnkey radio over Liquidsoap, AGPL | Rotation, once-per-X, time blocks, clockwheels | Request page + API, request-queue playlists | Not confirmed; likely always on | HLS audio | 0.24.0, 25 Oct |

Two facts in that table correct what the projects' docs imply. ErsatzTV Next's docs never mention idling, but its source does: a playlist request with no live session calls `ChannelSession::spawn`, waits up to 30 s for readiness, and a per-channel heartbeat file times out at **90 s** ([main.rs](https://raw.githubusercontent.com/ErsatzTV/next/main/crates/ersatztv/src/main.rs), [lib.rs](https://raw.githubusercontent.com/ErsatzTV/next/main/crates/ersatztv-core/src/lib.rs)). The 0.2.0 changelog states that `ersatztv-channel` "now stops after 90 seconds without a client" ([CHANGELOG](https://raw.githubusercontent.com/ErsatzTV/next/main/CHANGELOG.md)). A June 2026 comparison article claims ErsatzTV and Tunarr keep ffmpeg running for every channel all the time. The source code of both projects contradicts it ([pistack](https://www.pistack.xyz/posts/2026-06-12-self-hosted-virtual-tv-channels-ersatztv-tunarr-dizquetv/)).

ErsatzTV has just split in two. The old app (repo now `ErsatzTV/legacy`) keeps the library and scheduling UI. Since v26.11.0 it streams through the Next engine by default, and its own streaming engine is "removed in ErsatzTV 26.12" ([releases API](https://api.github.com/repos/ErsatzTV/ErsatzTV/releases)). Next runs standalone with `ersatztv /config/lineup.json`, serves `/channel/N.m3u8`, `/channels.m3u` and `/xmltv.xml` on port 8409, and requires ErsatzTV's own ffmpeg build ([Next README](https://raw.githubusercontent.com/ErsatzTV/next/main/README.md)). The README states plainly that "library and metadata management, scheduling and playout creation are not in scope". The bundled playout generator is "for demonstration and reference purposes". That is a drawback for anyone who wants a UI, and an advantage for anyone who wants to own the scheduling logic, which this use case demands.

The other tools fit less well. dizqueTV is in maintenance mode because its author moved to Tunarr ([pistack](https://www.pistack.xyz/posts/2026-06-12-self-hosted-virtual-tv-channels-ersatztv-tunarr-dizquetv/)). OBS can be scripted through obs-websocket and Advanced Scene Switcher, which even has a "media playlist with commercial interruptions" recipe ([ASS wiki](https://github.com/WarmUpTill/SceneSwitcher/wiki)). But OBS composites and encodes for as long as an output runs, and I found no viewer-aware idle mode, so it is the wrong shape for this.

## 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.

## 4. The request queue is a custom resolver

No video tool offers a real "play this next" queue with auto-fill from the library. The closest is ErsatzTV Next's `dynamic` source type, and it is a clean hook. A playout item whose source is `dynamic` is a placeholder. When playback reaches it, Next fetches a `PlayoutItem` JSON from a configured URI. It sends headers `x-etv-channel`, `x-etv-dynamic-id`, `x-etv-now` and `x-etv-until`, forces the returned item's start to the current transcode position, clamps its finish to the placeholder's end, and **calls your resolver again while playback is still inside the window**. One long placeholder can therefore yield a whole evening of items. A resolved item cannot itself be `dynamic`. The timeout defaults to 10 s, and retries are off unless you set `retry_budget_ms`. The feature has been in the playout schema since 0.0.1, and the retry fields since 0.0.6 ([playout reference](https://ersatztv.org/next-docs/reference/playout)). If a resolution fails, the channel's configured fallback plays (default 60 s) rather than dead air ([channel reference](https://ersatztv.org/next-docs/reference/channel)).

The other engines are clumsier. ffplayout's REST API has no insert endpoint. You read the day's playlist, splice in the request, and POST the whole document back, and edits only take effect after the next-but-one clip ([api.md](https://raw.githubusercontent.com/ffplayout/ffplayout/master/docs/api.md), [docs](https://ffplayout.github.io/)). Tunarr's channel API has CRUD, `now_playing`, and lineup and schedule routes, but nothing for queueing or playing next. `POST /channels/:id/programming` replaces the whole lineup, which also resets an on-demand cursor ([channelsApi.ts](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/api/channelsApi.ts)). Liquidsoap's `request.queue` is a real queue and works with video ([request sources](https://raw.githubusercontent.com/savonet/liquidsoap/v2.4.6/doc/content/request_sources.md), [HLS output](https://raw.githubusercontent.com/savonet/liquidsoap/v2.4.6/doc/content/hls_output.md)). But that means scripting Liquidsoap again, which is the route this project set out to avoid.

If requests rather than a schedule are the point, the **client-driven apps** remove the streaming problem altogether, because each friend's browser plays the file directly and the server only keeps a shared clock. **OpenTogetherTube** is maintained (commits through 22 Oct 2026). It plays HTTP MP4 and HLS/DASH VOD and has vote mode, vote-to-skip, DJ mode and per-room permissions ([README](https://raw.githubusercontent.com/dyc3/opentogethertube/master/README.md)). It cannot play live HLS, so it cannot carry the channel. Its natural role is a separate "request night" room pointed at the same normalized library. CyTube's custom manifests accept HLS, but its last human commit to the default branch was in **November 2025**, with only Dependabot bumps since ([commits API](https://api.github.com/repos/calzoneman/sync/commits?per_page=20)). Jellyfin SyncPlay has a group play queue with `Queue` and `QueueNext`, but no auto-fill when the queue runs out, and it transcodes separately for every viewer ([PlayQueueManager.cs mirror](https://git.sudo.is/mirrors/jellyfin/src/branch/master/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs)). On Discord, a normal bot cannot Go Live with video. The video bots (Discord-video-stream, StreamBot) need a user token, and Discord's policy says self-bots "can result in an account termination" ([Discord support](https://support.discord.com/hc/en-us/articles/115002192352-Automated-User-Accounts-Self-Bots)). Use Discord for the request commands, not for the video.

### The glue in four pieces

The sketch below is my design, built on the documented `dynamic` behavior. It is not an existing project.

The **library index** is a SQLite table populated by an ingest job that normalizes each file and records its ffprobe duration and tags: `episode`, `op`, `ed`, `cm`, `gag`, plus series and season. The ErsatzTV app uses the same idea for filler, with searches like `type:other_video duration:<90s` ([YAML schedules mirror](https://mintlify.wiki/ErsatzTV/legacy/scheduling/yaml-schedules)), and you can copy that tagging scheme.

The **queue store** is a second table: id, requester, media id, in and out points, status (queued, playing, done) and timestamps.

The **resolver** is a single HTTP endpoint. It reads `x-etv-now` and `x-etv-until`, takes the head of the queue if it fits in the remaining window, and otherwise returns a filler pick that fits, such as a random OP, an old commercial or a running-gag clip. It returns a `local` source with path and `in_point_ms`/`out_point_ms`, then marks the item as playing.

The **scheduler** writes the playout JSON for the day. Fixed blocks hold the scheduled anime episodes, with filler sequences between them, and long `dynamic` windows hold the request hours.

The **front end** is a ToS-compliant Discord bot with `/request`, `/queue` and `/np` that writes to the queue table, plus one web page with an hls.js player and the queue list.

The resolver is called only while the channel process is running, and Next stops the process when nobody watches. So **requests are consumed only while someone is actually watching**, and the queue behaves like Tunarr's paused cursor without extra code. That follows from the documented mechanics, but I have not tested it.

Three details matter in practice. First, clamping cuts off any item longer than the time left in its window. Either make request windows long, or have the resolver return filler when the next request will not fit. Second, Next's docs say nothing about skipping, so a `/skip` command would mean an unverified trick, such as regenerating the playout. Third, Next also accepts `http` (including live) and `rtsp` sources. A friend going live into the existing MediaMTX server could appear in the schedule or be returned by the resolver like any other item ([playout reference](https://ersatztv.org/next-docs/reference/playout)). I have not tested whether a live source behaves well inside a dynamic window.

## 5. Audio is solved by AzuraCast 0.24.0

If the channel drops to radio, **AzuraCast** needs no glue. Its playlists include general rotation and once-per-X-songs, -minutes or -hours, each of which can be limited to time blocks. That covers "a commercial every five songs" directly ([Playlists docs](https://www.azuracast.com/docs/user-guide/playlists/)). Requests come through a public listener page and an API ([docs](https://www.azuracast.com/docs/)). Version **0.24.0, released 25 Oct 2026**, adds nested playlists (clockwheels), **request-queue playlists** that can be scheduled as a source, a setting that blocks requests during scheduled blocks, and a per-user request cooldown. It ships Liquidsoap 2.4.6 ([CHANGELOG](https://raw.githubusercontent.com/AzuraCast/AzuraCast/main/CHANGELOG.md)). It deploys with Docker, outputs HLS audio, and has no video features. I could not confirm whether it ever idles. It almost certainly encodes continuously, but encoding one audio stream costs almost nothing, so idling matters much less here (my inference). For a Discord-native radio, Red-DiscordBot's Audio cog can play a `localtracks` folder (bot and Lavalink must run on the same host) and autoplay a playlist when the queue empties ([Red Audio docs](https://docs.discord.red/en/latest/cog_guides/audio.html)). Liquidsoap's cookbook has the one documented "pull only while listeners are connected" audio pattern, built on `output.harbor` connect and disconnect callbacks ([FFmpeg cookbook](https://www.liquidsoap.info/doc-dev/ffmpeg_cookbook.html)).

## 6. What I could not confirm

I did not install or run anything. Everything above comes from source code, docs and changelogs.

**Cold-start latency** is unmeasured for every option. Next waits up to 30 s for a channel to become ready. An older issue reported about 30 s startups on the Next engine inside the app, and a later release improved MPEG-TS start time ([issue #274](https://github.com/ErsatzTV/next/issues/274), [v26.11.3](https://api.github.com/repos/ErsatzTV/ErsatzTV/releases/tags/v26.11.3)). Test this before you trust it.

**Standalone Next's playout reloading** is not documented. I do not know whether it picks up an edited JSON file while a channel is playing. A v26.11.4 fix for Next channels "falling behind schedule when a playout was updated" suggests updates are read while a channel is live, but that fix was for the app-hosted engine.

**Glitches at item boundaries** with sub-minute clips have no benchmarks in any tool. Neither does whether hls.js copes with codec changes across discontinuities.

**Tunarr's on-demand mode** and its scheduler details are confirmed from source only. Its docs pages and API reference were unreachable.

**Idle behavior** is unconfirmed for Channel Engine, AzuraCast, and OpenTogetherTube's room clock.

**Shell wrapping of the `runOnDemand` command**, meaning whether MediaMTX runs it through `/bin/sh -c`, is unverified. Use `exec` in your script either way.

**Linux distribution packaging** for ErsatzTV Next, Tunarr and Liquidsoap 2.4.6 was not checked.

## 7. Conclusion

Most of the hard parts of a pseudo-TV channel now exist in open source: position math, idle shutdown, discontinuity-aware HLS, and filler between episodes. What is missing is the social layer, meaning who asked for what and what plays when the queue runs dry. ErsatzTV Next's decision to leave scheduling out of scope, and to expose an HTTP resolver instead, makes it the first engine where that layer is a small web service rather than a fork. The engineering left is a weekend of SQLite, a resolver endpoint and a Discord bot, not a Liquidsoap script.

The less obvious takeaway is that the idle problem largely goes away if you **normalize the library once**. With uniform media, every option becomes cheap. Next can stream-copy, a MediaMTX wrapper only remuxes during its idle tail, and a computed playlist runs nothing at all. Normalized files also play directly in OpenTogetherTube for request nights. Do that ingest step first, whichever engine you pick.

## 8. Sources

- [ErsatzTV Next source: main.rs](https://raw.githubusercontent.com/ErsatzTV/next/main/crates/ersatztv/src/main.rs)
- [ErsatzTV Next source: ersatztv-core lib.rs](https://raw.githubusercontent.com/ErsatzTV/next/main/crates/ersatztv-core/src/lib.rs)
- [ErsatzTV Next CHANGELOG](https://raw.githubusercontent.com/ErsatzTV/next/main/CHANGELOG.md)
- [pistack: Self-hosted virtual TV channels (ErsatzTV, Tunarr, dizqueTV)](https://www.pistack.xyz/posts/2026-06-12-self-hosted-virtual-tv-channels-ersatztv-tunarr-dizquetv/)
- [ErsatzTV releases (GitHub API)](https://api.github.com/repos/ErsatzTV/ErsatzTV/releases)
- [ErsatzTV Next README](https://raw.githubusercontent.com/ErsatzTV/next/main/README.md)
- [Advanced Scene Switcher wiki](https://github.com/WarmUpTill/SceneSwitcher/wiki)
- [ErsatzTV source: StartFFmpegSessionHandler.cs](https://raw.githubusercontent.com/ErsatzTV/ErsatzTV/main/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs)
- [ErsatzTV source: ChannelIdleBehavior.cs](https://raw.githubusercontent.com/ErsatzTV/ErsatzTV/main/ErsatzTV.Core/Domain/ChannelIdleBehavior.cs)
- [Tunarr source: StreamProgramCalculator.ts](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/stream/StreamProgramCalculator.ts)
- [Tunarr source: OnDemandChannelService.ts](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/services/OnDemandChannelService.ts)
- [Tunarr source: SessionManager.ts](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/stream/SessionManager.ts)
- [ErsatzTV Next docs: playout reference](https://ersatztv.org/next-docs/reference/playout)
- [MediaMTX default config: mediamtx.yml](https://raw.githubusercontent.com/bluenviron/mediamtx/main/mediamtx.yml)
- [MediaMTX source: HLS muxer.go](https://raw.githubusercontent.com/bluenviron/mediamtx/main/internal/servers/hls/muxer.go)
- [MediaMTX issue #3903](https://github.com/bluenviron/mediamtx/issues/3903)
- [MediaMTX source: HLS server.go](https://raw.githubusercontent.com/bluenviron/mediamtx/main/internal/servers/hls/server.go)
- [FFmpeg formats: concat demuxer](https://ffmpeg.org/ffmpeg-formats.html#concat)
- [MediaMTX source: externalcmd cmd.go](https://raw.githubusercontent.com/bluenviron/mediamtx/main/internal/externalcmd/cmd.go)
- [Eyevinn Channel Engine README](https://raw.githubusercontent.com/Eyevinn/channel-engine/master/README.md)
- [eyevinn-channel-engine (npm registry)](https://registry.npmjs.org/eyevinn-channel-engine)
- [nginx-vod-module README](https://raw.githubusercontent.com/kaltura/nginx-vod-module/master/README.md)
- [RFC 8216: HTTP Live Streaming](https://datatracker.ietf.org/doc/html/rfc8216)
- [ErsatzTV Next docs: channel reference](https://ersatztv.org/next-docs/reference/channel)
- [ffplayout API docs: api.md](https://raw.githubusercontent.com/ffplayout/ffplayout/master/docs/api.md)
- [ffplayout documentation](https://ffplayout.github.io/)
- [Tunarr source: channelsApi.ts](https://raw.githubusercontent.com/chrisbenincasa/tunarr/main/server/src/api/channelsApi.ts)
- [Liquidsoap 2.4.6 docs: request sources](https://raw.githubusercontent.com/savonet/liquidsoap/v2.4.6/doc/content/request_sources.md)
- [Liquidsoap 2.4.6 docs: HLS output](https://raw.githubusercontent.com/savonet/liquidsoap/v2.4.6/doc/content/hls_output.md)
- [OpenTogetherTube README](https://raw.githubusercontent.com/dyc3/opentogethertube/master/README.md)
- [CyTube (calzoneman/sync) commits (GitHub API)](https://api.github.com/repos/calzoneman/sync/commits?per_page=20)
- [Jellyfin SyncPlay PlayQueueManager.cs (mirror)](https://git.sudo.is/mirrors/jellyfin/src/branch/master/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs)
- [Discord support: Automated user accounts (self-bots)](https://support.discord.com/hc/en-us/articles/115002192352-Automated-User-Accounts-Self-Bots)
- [ErsatzTV legacy YAML schedules docs (mirror)](https://mintlify.wiki/ErsatzTV/legacy/scheduling/yaml-schedules)
- [AzuraCast docs: Playlists](https://www.azuracast.com/docs/user-guide/playlists/)
- [AzuraCast documentation](https://www.azuracast.com/docs/)
- [AzuraCast CHANGELOG](https://raw.githubusercontent.com/AzuraCast/AzuraCast/main/CHANGELOG.md)
- [Red-DiscordBot Audio cog docs](https://docs.discord.red/en/latest/cog_guides/audio.html)
- [Liquidsoap FFmpeg cookbook](https://www.liquidsoap.info/doc-dev/ffmpeg_cookbook.html)
- [ErsatzTV Next issue #274](https://github.com/ErsatzTV/next/issues/274)
- [ErsatzTV v26.11.3 release (GitHub API)](https://api.github.com/repos/ErsatzTV/ErsatzTV/releases/tags/v26.11.3)

---

Text licensed CC BY 4.0 (attribution: Hiina, ai.hiina.space). Source: https://github.com/hiinaspace/ai-hiina-space
