Skip to main content

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 $schema to 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 main and 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.

versionRequired

URI identifying the schema version, e.g. "https://ersatztv.org/playout/version/0.0.1".

itemsRequired

Ordered 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:

  1. Set source to use one shared source for every track. Track details (which stream, etc.) are chosen naively, or may be refined via tracks.
  2. Set tracks to specify each track (video, audio) individually. A track may provide its own source; if it doesn't, the item's top-level source is 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).

idRequired

Stable identifier for this item, unique within the playout.

startRequired

RFC3339 formatted start time, e.g. 2026-04-13T00:24:21.527-05:00.

finishRequired

RFC3339 formatted finish time, e.g. 2026-04-13T00:54:21.527-05:00.

source

The default source shared by any track that doesn't specify its own. Required unless every selected track in tracks provides its own source.

tracks

Per-track selection. Omit to let the server pick the first video and first audio track of the item's source.

watermark

Compatibility graphics layer composited below every entry in graphics. Omit for no legacy watermark.

graphics

Ordered 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[].tracks

Per-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).

video

Video track selection.

audio

Audio track selection.

subtitle

Subtitle track selection.

Track selection​

items[].tracks.video, items[].tracks.audio, items[].tracks.subtitle

Selects a single track (video or audio).

  • source absent: inherit the parent PlayoutItem.source.
  • source present: use this source for this track, overriding the parent.
  • stream_index absent: server picks the first stream of the track's kind in the effective source.
  • stream_index present: use this specific stream within the effective source.
source

Source to pull this track from. If omitted, inherits from the parent PlayoutItem.source.

stream_index

Zero-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[].source

A 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_typeRequired

Selects a local file source.

pathRequired

Absolute path to the media file.

in_point_ms

Optional start offset into the source, in milliseconds.

out_point_ms

Optional end offset into the source, in milliseconds.

probe_hint

Optional 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_typeRequired

Selects an ffmpeg lavfi (libavfilter virtual input) source.

paramsRequired

The lavfi filter graph parameters, passed verbatim to ffmpeg's -f lavfi -i.

probe_hint

Optional 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_typeRequired

Selects an HTTP or HTTPS source.

uriRequired
is_live

Whether the content is live and therefore cannot seek or work ahead. Default: false.

in_point_ms

Optional start offset into the source, in milliseconds.

out_point_ms

Optional end offset into the source, in milliseconds.

headers

Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"].

user_agent

Custom User-Agent string.

timeout_us

Socket timeout in microseconds.

reconnect

Enable reconnect on failure. Default: true.

reconnect_delay_max

Maximum reconnect delay in seconds. Maps to ffmpeg's reconnect_delay_max.

keep_alive

Enable persistent connections in ffmpeg. Default: false.

probe_hint

Optional 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_typeRequired

Selects an RTSP source.

uriRequired

RTSP URI template, e.g. "rtsp://user:{{PASSWORD}}@camera.lan:554/stream".

timeout_us

Socket timeout in microseconds.

probe_hint

Optional 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_typeRequired

Selects a command that writes MPEG-TS to stdout.

commandRequired

Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion.

args

Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to [].

is_live

Whether the content is live and therefore cannot work ahead. Default: false.

probe_hint

Optional 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 the start that will be forced onto the resolved item.
  • x-etv-until — the placeholder's finish; the resolver should not return an item that extends beyond this (it will be clamped if it does).
source_typeRequired

Selects a dynamic source, resolved over HTTP when the item plays.

uriRequired

URI template for the resolver endpoint, e.g. "http://localhost:8409/fallback?channel=5&token={{MY_SECRET}}". Must respond with a JSON PlayoutItem.

headers

Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} expansion.

user_agent

Custom User-Agent string. Supports {{TEMPLATE}} expansion.

timeout_us

Request 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_msUnreleased

Total 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_msUnreleased

Delay 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_hint

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

video

Video (and still-image) streams in the source. Omit (or use []) for audio-only sources; defaults to empty.

audio

Audio streams in the source. Omit (or use []) for video-only sources; defaults to empty.

subtitle

Subtitle 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_name

Container 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_ms

Source duration in milliseconds. Omit for live or unbounded sources.

Video hint​

source.probe_hint.video

Probe 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_indexRequired

Zero-based index of this stream within the source.

codecRequired

Codec name as reported by ffprobe (e.g. "h264", "hevc", "mpeg2video", "png"). Compared case-insensitively.

widthRequired

Coded frame width in pixels.

heightRequired

Coded frame height in pixels.

pix_fmtRequired

Pixel format (e.g. "yuv420p", "yuv420p10le").

frame_rate

Frame rate as a number or rational string (e.g. "24", "30000/1001"). Defaults to 24 when omitted.

profile

Codec profile (e.g. "high", "main 10"). Compared case-insensitively. Omit if unknown.

field_order

Interlacing field order as reported by ffprobe ("progressive", "tt", "bb", "tb", "bt"). Omit for progressive.

sample_aspect_ratio

Sample (pixel) aspect ratio, e.g. "1:1". Omit for square pixels.

display_aspect_ratio

Display aspect ratio, e.g. "16:9". Omit if unknown.

color_range

Color range (e.g. "tv", "pc"). Omit if unknown.

color_space

Color space / matrix coefficients (e.g. "bt709", "bt2020nc"). Omit if unknown.

color_transfer

Color 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_primaries

Color primaries (e.g. "bt709", "bt2020"). Omit if unknown.

dv_profile

Dolby 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_metadata

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

rotation

Display 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.audio

Probe metadata for a single audio stream.

stream_indexRequired

Zero-based index of this stream within the source.

codecRequired

Codec name as reported by ffprobe (e.g. "aac", "ac3", "mp2"). Compared case-insensitively.

channelsRequired

Number of audio channels.

Subtitle hint​

source.probe_hint.subtitle

Probe metadata for a single subtitle stream.

stream_indexRequired

Zero-based index of this stream within the source.

codecRequired

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

sourceRequired

The source providing the graphics media (typically an image, but any PlayoutItemSource is accepted).

stream_index

Zero-based stream index within the source. If omitted, the server picks the first video stream.

kind

Graphics layer kind.

locationRequired

Anchor position within the primary content frame.

width_percent

Scale the graphics layer to this percent of the primary content width (0–100). Omit to use its actual size.

horizontal_margin_percent

Horizontal offset from the anchor location, as a percent of primary content width (0–100). Omit for 0.

vertical_margin_percent

Vertical offset from the anchor location, as a percent of primary content height (0–100). Omit for 0.

opacity_percent

Opacity as a percent (0–100). Omit for fully opaque (100).

within_source_content

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

timing

Visibility schedule for the graphics layer. Omit for an always-on layer.

Timing​

items[].watermark.timing, items[].graphics[].timing

Controls 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_typeRequired

Selects periodic timing.

clockRequired

Reference clock used to position cycles.

frequency_msRequired

Period of the cycle, start-to-start, in milliseconds.

phase_offset_ms

Shift 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_ms

Cap 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_ms

Fade-in and fade-out duration in milliseconds (symmetric). Must be ≤ hold_ms. Omit for 1000; set to 0 for hard cuts.

hold_msRequired

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

ValueSince
media0.0.4
canvas0.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.

ValueSince
top_left0.0.1
top_center0.0.1
top_right0.0.1
center_left0.0.1
center0.0.1
center_right0.0.1
bottom_left0.0.1
bottom_center0.0.1
bottom_right0.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.

ValueSince
wall0.0.1
content0.0.1