Playout
A playout file lists what a channel plays during one time window. The channel reads them from its playout folder
(playout.folder in channel.json) and loads the file that covers the current time.
Playouts are written by a scheduler, such as ErsatzTV (legacy).
- Every playout file must have
version. See Config Versions. - Times are RFC 3339 with an offset, e.g.
2026-04-13T20:00:00.000-05:00. - For validation and completion in an editor such as VS Code, set
$schemato the JSON schema,https://ersatztv.org/schema/playout/0.0.6.json. ErsatzTV Next ignores$schema. - Since is the first playout version with the property. A property marked unreleased is on
mainand not in a release yet.
{
"version": "https://ersatztv.org/playout/version/0.0.6",
"items": [
{
"id": "1",
"start": "2026-04-13T20:00:00.000-05:00",
"finish": "2026-04-13T20:30:00.000-05:00",
"source": { "source_type": "local", "path": "/media/shows/episode-1.mkv" }
}
]
}
Top level
A playout schedule for a single time window.
Files should be named {start}_{finish}.json using compact ISO 8601 (no separators), e.g. 20260413T000000.000000000-0500_20260414T002131.620000000-0500.json, so that the channel can locate the correct file for the current time.
versionRequiredURI identifying the schema version, e.g. "https://ersatztv.org/playout/version/0.0.1".
itemsRequiredOrdered list of scheduled items for this window.
Item
items[]A single scheduled item in the playout.
An item must supply media for its tracks. You can do this two ways, and they can be combined:
- Set
sourceto use one shared source for every track. Track details (which stream, etc.) are chosen naively, or may be refined viatracks. - Set
tracksto specify each track (video, audio) individually. A track may provide its ownsource; if it doesn't, the item's top-levelsourceis used.
At least one of source or tracks must be present, and every track that is selected must have an effective source (either its own or the item's).
idRequiredStable identifier for this item, unique within the playout.
startRequiredRFC3339 formatted start time, e.g. 2026-04-13T00:24:21.527-05:00.
finishRequiredRFC3339 formatted finish time, e.g. 2026-04-13T00:54:21.527-05:00.
sourceThe default source shared by any track that doesn't specify its own. Required unless every selected track in tracks provides its own source.
tracksPer-track selection. Omit to let the server pick the first video and first audio track of the item's source.
watermarkCompatibility graphics layer composited below every entry in graphics. Omit for no legacy watermark.
graphicsOrdered media-backed graphics layers. The first entry is lowest and the last entry is highest; a compatibility watermark is always below this array.
Tracks
items[].tracksPer-track overrides for a playout item. Omit a field to use the server default for that track kind (first stream of that kind in the item's source, if any).
videoVideo track selection.
audioAudio track selection.
subtitleSubtitle track selection.
Track selection
items[].tracks.video, items[].tracks.audio, items[].tracks.subtitleSelects a single track (video or audio).
sourceabsent: inherit the parentPlayoutItem.source.sourcepresent: use this source for this track, overriding the parent.stream_indexabsent: server picks the first stream of the track's kind in the effective source.stream_indexpresent: use this specific stream within the effective source.
sourceSource to pull this track from. If omitted, inherits from the parent PlayoutItem.source.
stream_indexZero-based stream index within the effective source. If omitted, the server picks the first stream of this track's kind.
Source
items[].source, items[].tracks.*.source, items[].watermark.source, items[].graphics[].sourceA media source. Exactly one variant, distinguished by source_type.
One of:
Local files
source_type: "local"A file on the local filesystem reachable by the server.
source_typeRequiredSelects a local file source.
pathRequiredAbsolute path to the media file.
in_point_msOptional start offset into the source, in milliseconds.
out_point_msOptional end offset into the source, in milliseconds.
probe_hintOptional pre-supplied probe metadata. When present, ffprobe is skipped and the source is read only once (at playback). See ProbeHint.
Lavfi
source_type: "lavfi"A synthetic source produced by an ffmpeg lavfi filter graph.
source_typeRequiredSelects an ffmpeg lavfi (libavfilter virtual input) source.
paramsRequiredThe lavfi filter graph parameters, passed verbatim to ffmpeg's -f lavfi -i.
probe_hintOptional pre-supplied probe metadata. When present, ffprobe is skipped and the source is read only once (at playback). See ProbeHint.
HTTP
source_type: "http"A remote source fetched over HTTP(S).
source_typeRequiredSelects an HTTP or HTTPS source.
uriRequiredURI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}".
is_liveWhether the content is live and therefore cannot seek or work ahead. Default: false.
in_point_msOptional start offset into the source, in milliseconds.
out_point_msOptional end offset into the source, in milliseconds.
headersCustom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"].
user_agentCustom User-Agent string.
timeout_usSocket timeout in microseconds.
reconnectEnable reconnect on failure. Default: true.
reconnect_delay_maxMaximum reconnect delay in seconds. Maps to ffmpeg's reconnect_delay_max.
keep_aliveEnable persistent connections in ffmpeg. Default: false.
probe_hintOptional pre-supplied probe metadata. When present, ffprobe is skipped and the source is read only once (at playback). See ProbeHint.
RTSP
source_type: "rtsp"A live stream pulled from an RTSP server (e.g. an IP camera). Always treated as live: it is never seeked and cannot work ahead.
source_typeRequiredSelects an RTSP source.
uriRequiredRTSP URI template, e.g. "rtsp://user:{{PASSWORD}}@camera.lan:554/stream".
timeout_usSocket timeout in microseconds.
probe_hintOptional pre-supplied probe metadata. When present, ffprobe is skipped and the source is read only once (at playback). See ProbeHint.
Script
source_type: "script"An external command whose stdout is an MPEG-TS stream, proxied to ffmpeg over loopback HTTP.
source_typeRequiredSelects a command that writes MPEG-TS to stdout.
commandRequiredCommand that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion.
argsOptional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to [].
is_liveWhether the content is live and therefore cannot work ahead. Default: false.
probe_hintOptional pre-supplied probe metadata. When present, ffprobe is skipped and the source is read only once (at playback). Especially useful here: a scripted source (e.g. a yt-dlp pipeline) is otherwise opened twice, once to probe and once to play. See ProbeHint.
Dynamic
source_type: "dynamic"A placeholder source resolved at playback time by fetching a PlayoutItem JSON document over HTTP(S). The returned item replaces this one; its start is forced to the current transcode position and its finish is clamped to the placeholder's finish. Because start advances each tick, the resolver is re-hit while the transcode position remains inside the placeholder window, allowing a sequence of distinct items to be returned for a single placeholder. The resolved item's source may not itself be dynamic, but it may carry its own probe_hint, which is honored exactly as for a directly-specified source.
The channel automatically injects the following request headers (in addition to any configured via headers), all formatted per RFC 3339 for timestamps:
x-etv-channel— the channel number this request is for.x-etv-dynamic-id— a stable identifier for the placeholder; resolvers can use it to coalesce or cache decisions across the multiple calls that occur while transcoding stays inside one placeholder window.x-etv-now— the current transcode position; this is thestartthat will be forced onto the resolved item.x-etv-until— the placeholder'sfinish; the resolver should not return an item that extends beyond this (it will be clamped if it does).
source_typeRequiredSelects a dynamic source, resolved over HTTP when the item plays.
uriRequiredURI template for the resolver endpoint, e.g. "http://localhost:8409/fallback?channel=5&token={{MY_SECRET}}". Must respond with a JSON PlayoutItem.
headersCustom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} expansion.
user_agentCustom User-Agent string. Supports {{TEMPLATE}} expansion.
timeout_usRequest timeout in microseconds. Defaults to 10 seconds. With retry enabled, each attempt's timeout is the smaller of this and the retry budget left.
retry_budget_msUnreleasedTotal time in milliseconds to spend resolving this source, including every attempt and the waits between them. Absent or 0 disables retry. Transport errors, timeouts and 5xx responses are retried; 4xx responses and a body that isn't a valid PlayoutItem are not. When resolution still fails, the channel plays its fallback for at least this long (see fallback.duration_seconds in the channel config). Retries delay the first segment when a channel starts while the resolver is unreachable.
retry_backoff_msUnreleasedDelay in milliseconds before the first retry. Defaults to 250. The delay doubles after each retry, with jitter, and a retry is skipped when its delay would use up the budget.
Probe hint
source.probe_hintPre-supplied probe metadata for a source. When present, the server trusts these values and skips running ffprobe entirely, so the source is opened only once (at playback) instead of twice. This matters most for slow or expensive inputs such as scripted yt-dlp pipelines.
The hint fully replaces probing: values are not validated up front, so incorrect metadata surfaces as an ffmpeg error during playback rather than a probe failure. Provide an entry for every stream the pipeline needs to select — typically one video and one audio, plus any subtitle stream a track selects.
videoVideo (and still-image) streams in the source. Omit (or use []) for audio-only sources; defaults to empty.
audioAudio streams in the source. Omit (or use []) for video-only sources; defaults to empty.
subtitleSubtitle streams in the source. Omit (or use []) for sources without subtitles; defaults to empty. Provide an entry for any subtitle stream a track selects, otherwise the requested subtitle stream cannot be located and subtitles are dropped.
format_nameContainer format name as reported by ffprobe's format_name (e.g. "mpegts", "image2", "png_pipe"). Used to detect still images (a single video stream in an image2 or *_pipe container). Defaults to "mpegts" when omitted.
duration_msSource duration in milliseconds. Omit for live or unbounded sources.
Video hint
source.probe_hint.videoProbe metadata for a single video (or still-image) stream. Optional fields left out assume progressive, square-pixel, SDR content at 24 fps — the same fallbacks used when ffprobe omits a field.
stream_indexRequiredZero-based index of this stream within the source.
codecRequiredCodec name as reported by ffprobe (e.g. "h264", "hevc", "mpeg2video", "png"). Compared case-insensitively.
widthRequiredCoded frame width in pixels.
heightRequiredCoded frame height in pixels.
pix_fmtRequiredPixel format (e.g. "yuv420p", "yuv420p10le").
frame_rateFrame rate as a number or rational string (e.g. "24", "30000/1001"). Defaults to 24 when omitted.
profileCodec profile (e.g. "high", "main 10"). Compared case-insensitively. Omit if unknown.
field_orderInterlacing field order as reported by ffprobe ("progressive", "tt", "bb", "tb", "bt"). Omit for progressive.
sample_aspect_ratioSample (pixel) aspect ratio, e.g. "1:1". Omit for square pixels.
display_aspect_ratioDisplay aspect ratio, e.g. "16:9". Omit if unknown.
color_rangeColor range (e.g. "tv", "pc"). Omit if unknown.
color_spaceColor space / matrix coefficients (e.g. "bt709", "bt2020nc"). Omit if unknown.
color_transferColor transfer characteristics (e.g. "bt709", "smpte2084", "arib-std-b67"). The values "smpte2084" and "arib-std-b67" mark the stream as HDR. Omit for SDR.
color_primariesColor primaries (e.g. "bt709", "bt2020"). Omit if unknown.
dv_profileDolby Vision profile from the stream's DOVI configuration record (e.g. 5, 7, 8). Profile 5 has no backward-compatible base layer and is not described by the color_* fields; profiles 4, 7 and 8 do, so those are handled from color_transfer alone. Omit for non-Dolby Vision streams.
has_hdr10_metadataWhether the stream carries HDR10 static metadata (mastering display colour volume or content light level), at the container level or in per-frame SEI. Hardware tonemappers that read that metadata (Intel vpp_qsv) are only used when this is true; omitting it is treated as false and falls back to software tonemapping for "smpte2084" content.
rotationDisplay rotation in counter-clockwise degrees from the stream's display matrix, normalized to 0, 90, 180, or 270. The channel rotates frames explicitly after deinterlacing and before scaling, using hardware filters when supported and software otherwise. A 90/270 rotation swaps width and height and inverts pixel and display aspect ratios. Omit when unknown; for local sources the channel then reads it from the file header.
Audio hint
source.probe_hint.audioProbe metadata for a single audio stream.
stream_indexRequiredZero-based index of this stream within the source.
codecRequiredCodec name as reported by ffprobe (e.g. "aac", "ac3", "mp2"). Compared case-insensitively.
channelsRequiredNumber of audio channels.
Subtitle hint
source.probe_hint.subtitleProbe metadata for a single subtitle stream.
stream_indexRequiredZero-based index of this stream within the source.
codecRequiredCodec name as reported by ffprobe (e.g. "subrip", "ass", "hdmv_pgs_subtitle", "dvd_subtitle"). Compared case-insensitively; image-based codecs are handled differently from text-based ones.
Graphics layer
items[].watermark, items[].graphics[]An image or video graphics layer composited on top of the primary content. Sized and positioned relative to the primary content's frame.
sourceRequiredThe source providing the graphics media (typically an image, but any PlayoutItemSource is accepted).
stream_indexZero-based stream index within the source. If omitted, the server picks the first video stream.
kindGraphics layer kind.
locationRequiredAnchor position within the primary content frame.
width_percentScale the graphics layer to this percent of the primary content width (0–100). Omit to use its actual size.
horizontal_margin_percentHorizontal offset from the anchor location, as a percent of primary content width (0–100). Omit for 0.
vertical_margin_percentVertical offset from the anchor location, as a percent of primary content height (0–100). Omit for 0.
opacity_percentOpacity as a percent (0–100). Omit for fully opaque (100).
within_source_contentWhen true, position margins are measured from the edges of the source content rather than the padded output frame, so letterbox/pillarbox bars push the graphics layer inward and keep it inside the visible content. When false, margins are relative to the full padded frame, so a 0% margin can land inside the bars. Has no effect when the primary content fills the output (crop/stretch). Omit for false.
timingVisibility schedule for the graphics layer. Omit for an always-on layer.
Timing
items[].watermark.timing, items[].graphics[].timingControls when the graphics layer is shown. Exactly one variant, distinguished by timing_type.
One of:
Periodic timing
timing_type: "periodic"Cyclical visibility: the graphics layer fades in, holds, and fades out once every frequency_ms. Cycle length is start-to-start, so 2 * fade_ms + hold_ms must be ≤ frequency_ms, and fade_ms must be ≤ hold_ms.
timing_typeRequiredSelects periodic timing.
clockRequiredReference clock used to position cycles.
frequency_msRequiredPeriod of the cycle, start-to-start, in milliseconds.
phase_offset_msShift the cycle by this many milliseconds. With clock: wall and frequency_ms: 300000, an offset of 0 puts an appearance start at every wall-clock 5-minute boundary; 120000 shifts to :02, :07, :12, etc. Omit for 0.
disable_after_msCap on the time, measured from the start of the playout item in milliseconds, during which new appearances are allowed to begin. An appearance already in progress at the cap is allowed to fade out cleanly. Omit for no cap.
fade_msFade-in and fade-out duration in milliseconds (symmetric). Must be ≤ hold_ms. Omit for 1000; set to 0 for hard cuts.
hold_msRequiredTime held fully visible between fade-in and fade-out, in milliseconds.
Values
The allowed values for properties that take a fixed set of strings.
Graphics layer kind
canvas: a full-frame layer whose frames are already the output size and carry alpha. It is composited at (0,0) and is content-locked: the channel seeks it to its own position in the item. location, margins, width_percent, within_source_content, opacity_percent and timing are ignored (a warning is logged if present). HTTP canvas sources receive x-etv-channel, x-etv-offset-ms, x-etv-duration-ms and x-etv-frame-rate headers.
| Value | Since |
|---|---|
media | 0.0.4 |
canvas | 0.0.4 |
Graphics location
Nine-position anchor within the primary content frame. Read like a 3×3 grid: rows top/center/bottom, columns left/center/right; the dead center is center.
| Value | Since |
|---|---|
top_left | 0.0.1 |
top_center | 0.0.1 |
top_right | 0.0.1 |
center_left | 0.0.1 |
center | 0.0.1 |
center_right | 0.0.1 |
bottom_left | 0.0.1 |
bottom_center | 0.0.1 |
bottom_right | 0.0.1 |
Periodic clock
Reference clock for periodic timing. wall aligns cycles to wall-clock time (so a viewer tuning in at any moment sees the same phase). content measures from the start of the containing playout item.
| Value | Since |
|---|---|
wall | 0.0.1 |
content | 0.0.1 |