Playout
playout configures generated channels — your local media played as 24/7 live TV. It holds two
things:
- Transcode — the FFmpeg command that turns local media into a continuous live stream.
- Scheduling & listing — refresh interval, daily swap time, logo, state directory, and more.
Settings cascade Global ➡ Playlist ➡ Channel: define them once at the top, and a channel overrides only what
differs. A channel itself carries just name, sources, fields, and its playout override.
Like the proxy segmenter, playout runs once per channel and is shared by all viewers (the first viewer's config wins). The difference is the input: the segmenter pulls a remote stream, while playout reads your local files.
How It Works
Playout runs your command once per file. A supervisor resolves the channel's current file from its
persisted schedule, runs the command for that file, and advances to the next when it
finishes — looping forever. Every run writes into one shared HLS directory, so viewers get a single continuous stream.
Per-file invocation is what lets the command adapt to each file via the
Playout.* variables; a single whole-schedule process could not.
The command takes one input and produces one output:
- Input:
{{ .Playout.Input }}, seeked to{{ .Playout.Offset }}(seconds). The file's probed parameters (VideoCodec,Width,AudioLanguages, …) are exposed too, so the command can pick its decoder, scaling, or audio track per file — see Reserved Template Variables. - Output: a rolling HLS stream at
{{ .Stream.PlaylistPath }}with segments at{{ .Stream.SegmentPath }}.
Any command that does this works; the FFmpeg flags are up to you. The next section lists what a correct command must handle.
No default command
There is no default command — a transcode that keeps up in realtime depends on your hardware (CPU vs. a GPU
encoder like NVENC/VAAPI/QSV), so majmun cannot pick one for you. If channels are configured but no command is set
(at the global, playlist, or channel level), startup fails with an error. Start from a ready-to-use command in
Examples → Playout and adjust it to your hardware.
What a command must take care of
A correct command handles all of these.
- Realtime pacing — emit ~1 second of video per second of wall time (
-re). Without it the command races through the file as fast as it can encode, flooding the disk with segments. - Normalization — scale/letterbox every source to one
width×height×fpscanvas and normalize audio, so the stream stays uniform across files. Skip this only if all sources are already uniform. - Appending to the live playlist — each file's process writes into the same HLS directory, so the command must
append rather than truncate (
-hls_flags append_list+omit_endlist) and mark a discontinuity at the join (discont_start). - Segment alignment — keyframes on segment boundaries, so joins are fast and segments uniform.
- Buffering — realtime pacing leaves only ~one segment of headroom, and majmun waits for
init_segmentssegments before serving.-readrate_initial_burstemits the first few seconds at full speed to build that buffer without delaying start.
YAML Structure
playout:
command: []
template_variables: []
env_variables: []
init_segments: 4
ready_timeout: 30s
state_dir: state
logo: ""
extensions: []
order: sequential
refresh_interval: 30m
epg_duration: 1w
schedule_swap_at: "04:00"
metadata:
title: '{{ .Probe.Title | default .File.Name }}'
description: '{{ .Probe.Description }}'
category: '{{ .Probe.Category }}'
filler:
sources: []
every: 1h
max_duration: 0s
order: shuffle
metadata:
title: Advertising
playlists:
- name: local
playout: {} # playlist-level overrides (any subset of the above)
channels:
- name: cartoons
sources: [/media/cartoons]
playout: {} # channel-level overrides
Fields
Transcode
| Field | Type | Required | Description |
|---|---|---|---|
command |
Command |
Yes | Transcoder command. No default — see How It Works. |
template_variables |
[]NameValue |
No | User variables for the command. Merged by name across cascade levels. |
env_variables |
[]NameValue |
No | Environment variables for the command. Merged by name across cascade levels. |
init_segments |
int |
No | Number of segments that must exist before clients can start reading (default: 4). Must be at least 1. |
ready_timeout |
duration |
No | Maximum time to wait for the initial segments to become available (default: 30s). |
Scheduling & Listing
| Field | Type | Required | Description |
|---|---|---|---|
state_dir |
string |
No | Directory where channel schedules are persisted. Default state. |
logo |
string |
No | Channel logo: an http(s) URL or a local file path. Available in commands as {{ .Channel.Logo }}. |
extensions |
[]string |
No | Video file extensions to include. Default: mkv, mp4, avi, mov, m4v, ts, webm, mpg, mpeg, flv, wmv. |
order |
string |
No | Playback order: sequential (episode order), shuffle (stable random), interleave (round-robin episodes across shows; shorter shows finish first), or spread (distribute each show's episodes evenly across the whole timeline so all shows run start-to-finish). Default sequential. |
season_patterns |
[]regex |
No | Patterns matching season folder names, used to group shows (for interleave/spread) and to fill {{ .Probe.Season }}. Defaults cover Season N / Сезон N / S01 and bare numbers. |
episode_patterns |
[]regex |
No | Patterns to extract season/episode numbers from the tag and filename, used for sorting and metadata ({{ .Probe.Season }} / {{ .Probe.Episode }}). Two capture groups yield (season, episode), one yields the episode. Defaults cover S01E05, 1x05, ep05, a leading number followed by a dot (20. Title), and similar. |
refresh_interval |
duration |
No | How often sources is re-scanned for added/removed files (see adopting changes). Default 30m; 0 disables re-scanning. |
epg_duration |
duration |
No | How far into the future the EPG is generated. Default 1w. |
schedule_swap_at |
string (HH:MM) |
No | Local time of day after which a changed file set is adopted by the live stream — deferred further to the end of the programme then playing, so a show is never cut off mid-way. Default 04:00. |
metadata |
Metadata |
No | Go templates building the EPG title, description, and category per file. Defaults reproduce the container-tag behavior. |
filler |
Filler |
No | Inject filler clips between content. Off unless sources is set. See Filler. |
Season/episode detection
Season and episode numbers are resolved per file in this order, stopping at the first hit:
- Container tags —
episode_id/episode_sort/episode, parsed withepisode_patterns. - Filename — parsed with
episode_patterns(e.g.S01E05,1x05,20. Title). - Season folder — the season number from the
season_patternsfolder fills the season if still unset.
A file with no detectable numbers keeps season/episode 0; ordering then falls back to natural filename sort within the directory.
Metadata Templates
metadata holds Go templates (with sprig functions) that build each file's EPG
fields. They are evaluated when the guide is generated, so editing them takes effect on the next EPG request without a
rebuild. Each subkey is optional; omitted keys use the defaults below, which match the
container-tag behavior.
| Field | Default | Description |
|---|---|---|
title |
{{ .Probe.Title \| default .File.Name }} |
EPG programme title. |
description |
{{ .Probe.Description }} |
EPG programme description (<desc>). |
category |
{{ .Probe.Category }} |
EPG programme category (<category>). |
date and episode are derived in fixed ways and are not templatable. If a template fails to render for a file,
the raw tag value is used instead and the error is logged.
Templates receive:
| Variable | Description |
|---|---|
{{ .Probe.Title }} |
Raw title tag (may be empty). |
{{ .Probe.Description }} |
Raw description/synopsis/summary/comment tag. |
{{ .Probe.Category }} |
Raw genre tag. |
{{ .Probe.Date }} |
Normalized date (YYYYMMDD) when known. |
{{ .Probe.Season }} / {{ .Probe.Episode }} |
Parsed season/episode numbers (0 when unknown). |
{{ .Probe.VideoCodec }}, {{ .Probe.Width }}, {{ .Probe.Height }}, … |
The file's probed media parameters (same set as the Playout.* command variables). |
{{ .File.Path }} |
Absolute file path. |
{{ .File.Rel }} |
Path relative to the source root that contains the file, e.g. Show/S01E05.mkv. |
{{ .File.RelNoExt }} |
Path relative to the source root, without the file extension, e.g. Show/Season 1/20. Title. |
{{ .File.Name }} |
File name without its extension. |
{{ .File.Source }} |
The configured source root that contains the file. |
{{ .File.SourceBase }} |
Base name of the source root, e.g. My Show for source /media/series/My Show. Useful as the series name when each source is a single show. |
For a channel that merges several shows, prefix the show folder so episodes are distinguishable:
playout:
metadata:
title: '{{ .File.Rel | splitList "/" | first }} — {{ .Probe.Title | default .File.Name }}'
Filler
filler inserts breaks of clips from sources between content. Clips play through the same command as content. Set
sources to enable filler; everything else is optional.
| Field | Type | Required | Description |
|---|---|---|---|
sources |
[]string |
Yes | Directories or files holding filler clips. Scanned with the same extensions as content. |
every |
duration |
No | Break after roughly this much content playtime (rounded up to a whole item). Default 1h; excludes every_count. |
every_count |
int |
No | Break after every this many content items. Excludes every. |
max_duration |
duration |
No | Cap on filler time per break. 0 (default) plays one clip per break. |
order |
string |
No | Order clips are drawn in: shuffle (default) or sequential. |
metadata |
Metadata |
No | EPG title (default Advertising) and category for breaks. |
In the EPG, each break shows as a single programme (e.g. Advertising 18:50–18:53) with no season/episode/description.
If sources holds more clips than the breaks in one loop can play, the pool rotates: each schedule rebuild (when files
or config change) resumes from where the last one stopped, so every clip eventually airs.
Reserved Template Variables
The playout command receives these runtime variables (also as MAJMUN_* environment variables) — see
Command → Reserved Variables for the full reference:
| Variable | Environment variable | Description |
|---|---|---|
{{ .Playout.Input }} |
MAJMUN_PLAYOUT_INPUT |
Path of the file to play now. |
{{ .Playout.Offset }} |
MAJMUN_PLAYOUT_OFFSET |
Seek position (seconds) into the file for the live point. Empty at the file's start. |
{{ .Playout.VideoCodec }} |
MAJMUN_PLAYOUT_VIDEO_CODEC |
The file's video codec (e.g. h264, hevc), for selecting a decoder. Empty if unknown. |
{{ .Playout.Width }} / {{ .Playout.Height }} |
MAJMUN_PLAYOUT_WIDTH / MAJMUN_PLAYOUT_HEIGHT |
Coded video dimensions in pixels, as stored in the file. |
{{ .Playout.AspectWidth }} |
MAJMUN_PLAYOUT_ASPECT_WIDTH |
Width corrected for the sample aspect ratio: the square-pixel display width (even-rounded). Equals Width for square-pixel sources; larger/smaller for anamorphic ones. Use as the scale_vaapi width to bake the aspect into the pixels without a software setsar/setdar. |
{{ .Playout.PixelFormat }} |
MAJMUN_PLAYOUT_PIXEL_FORMAT |
Video pixel format (e.g. yuv420p). |
{{ .Playout.FrameRate }} |
MAJMUN_PLAYOUT_FRAME_RATE |
Video frame rate as a fraction (e.g. 30000/1001). |
{{ .Playout.FieldOrder }} |
MAJMUN_PLAYOUT_FIELD_ORDER |
Field order (progressive, tt, bb, …), for deinterlace decisions. |
{{ .Playout.AudioCodec }} |
MAJMUN_PLAYOUT_AUDIO_CODEC |
First audio stream codec (e.g. aac). |
{{ .Playout.AudioChannels }} |
MAJMUN_PLAYOUT_AUDIO_CHANNELS |
First audio stream channel count. |
{{ .Playout.SampleRate }} |
MAJMUN_PLAYOUT_SAMPLE_RATE |
First audio stream sample rate in Hz. |
{{ .Playout.AudioLanguages }} |
MAJMUN_PLAYOUT_AUDIO_LANGUAGES |
Space-separated language of each audio stream, in order (und when untagged), e.g. eng rus und. |
{{ .Stream.SegmentPath }} |
MAJMUN_STREAM_SEGMENT_PATH |
Output segment file path pattern. |
{{ .Stream.PlaylistPath }} |
MAJMUN_STREAM_PLAYLIST_PATH |
Output HLS playlist path. |
{{ .Channel.Name }} |
MAJMUN_CHANNEL_NAME |
Channel name. |
{{ .Channel.Logo }} |
MAJMUN_CHANNEL_LOGO |
Channel logo path/URL, e.g. -i "{{ .Channel.Logo }}" for a watermark. |
{{ .Playlist.Name }} |
MAJMUN_PLAYLIST_NAME |
Parent playlist name. |
Examples
See Examples → Playout for full, ready-to-use configs: basic channels, the cascade override, a software transcode, a logo watermark, and hardware-accelerated (NVENC / VAAPI / QSV) variants.