Markdown version: https://ai.hiina.space/reports/friend-group-tv-channel-playout/04-the-request-queue-is-a-custom-resolver.md ยท index

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

Friend group TV channel playout: 4. The request queue is a custom resolver

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). If a resolution fails, the channel's configured fallback plays (default 60 s) rather than dead air (channel reference).

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, docs). 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). Liquidsoap's request.queue is a real queue and works with video (request sources, HLS output). 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). 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). 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). 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). 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), 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). I have not tested whether a live source behaves well inside a dynamic window.


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