A channel is Kiln’s core object. It combines an upstream address, an input format, and a run policy, then exposes one playback URL and one entry in the playlist. The sections below follow the order of the configuration fields.
The channel model
Identity and presentation
id: the unique identifier. It appears in playback URLs, in the playlist’stvg-idattribute, and in access logs. Changing it creates a different channel as far as players are concerned, so favorites and history on the client side are lost.title: display name. Falls back toidwhen empty.group: group name, written to the playlist’sgroup-titleattribute.logo_url: logo address. When it is empty and a built-in logo candidate matches, the playlist points at/v1/logo/{id}instead and Kiln fetches the image for you.epg_id,epg_name,epg_source: guide matching fields.epg_namefalls back totitle. See EPG.
Stream source
There are two ways to point at a source, and you pick exactly one:
upstream+path:upstreamreferences anidfrom[[upstreams]], andpathis appended to that upstream’sbase_url. Apathstarting withhttp://orhttps://is treated as absolute and used as is.source_url: a complete address written inline. It replaces bothupstreamandpath, and a channel using it inherits no upstream-level headers.
ingress decides how the source is handled. It is hls or dash, and defaults to hls when empty:
hls: same-origin segment proxying. The playlist is rewritten before it reaches the player and segments are fetched on demand.dash: decrypted locally against the globalkid:keycatalog and repackaged to HLS. An enabled DASH channel requires[packager].keys_fileto be configured, otherwise saving fails.
Run policy
on_demand: release the upstream connection when nobody is watching. Enabling it puts the channel under idle reclamation.autostart: pull the channel up when the process starts, without waiting for a viewer.idle_timeout_sec: how long counts as nobody watching. Values of zero or below normalize to90seconds. Only applies toon_demandchannels.max_viewers: concurrent viewer ceiling. Above zero it enables viewer leases and returns 429 once the ceiling is hit; empty or zero means unlimited.restart_on_failure: restart automatically after the upstream drops. Forced on for DASH channels at save time.
The two switches combine into the three run modes shown in the admin console:
autostart |
on_demand |
Console label | Behavior |
|---|---|---|---|
| off | on | Start when viewed | The first viewer starts it, idle reclaims it |
| on | on | Prewarm at startup | Started at boot, still reclaimed when idle |
| on | off | Always running | Started at boot, never reclaimed |
With both switches off, on_demand is turned on automatically, so there is no such thing as a channel that neither prewarms nor starts on demand.
Media selection
These fields apply only to the DASH repackaging path. HLS channels are proxied same-origin and no track plan is made:
prefer_height: target vertical resolution. Zero or empty falls back to[ffmpeg].prefer_height.preferred_audio_languages: audio language codes in priority order, for example["zh", "en"].selection: finer-grained track selection with a separate mode and track for video, audio, and subtitles.packager: sets the packaging engine for this channel and accepts the same values as[packager].engine. Leave it empty to use the global setting. Any other value fails configuration validation.
Fine-grained track selection
Video can be selected automatically, capped at a maximum resolution, or matched to one exact track. Audio can follow a language preference or accept only a specified track. Subtitles can also be turned off. Selectors can match key, language, role, codec, resolution, or DASH AdaptationSet and Representation IDs. The exact and only modes require at least one selector field.
With the ffmpeg engine, subtitles support auto and off only. See the channel configuration reference for every field and accepted value.
Request headers
user_agent: the User-Agent used when fetching this channel.headers: extra request headers for this channel. Merged with the upstream-levelheaders, with the channel winning on duplicate keys.
Fixed headers are sent only to requests on the source origin. Cross-origin redirects and external playlist resources do not receive them. The HTTPS FFmpeg compatibility path rejects a request when it cannot preserve that origin boundary, preventing credential leakage.
Disabling
A channel with disabled set does not appear in the channel list or the playlist, and playback requests are answered as not found. A disabled DASH channel can be saved even before the global key file exists, which makes it easy to land the definition first and supply keys later.
Upstream definitions
Upstreams are defined in the configuration file only. They let several channels share one origin’s address and credentials:
[[upstreams]]
id = "origin"
base_url = "http://127.0.0.1:5050"
[upstreams.headers]
X-Custom = "value"id: what a channel’supstreamfield references.base_url: the origin’s base address; a channel’spathis appended to it.headers: request headers injected into every fetch for channels on this upstream. A channel’s ownheaderscan override individual keys.
The matching channel:
[[channels]]
id = "hls-demo"
title = "Demo HLS"
group = "Demo"
upstream = "origin"
path = "/live/demo-hls"
ingress = "hls"
on_demand = true
autostart = falseConfig file versus admin console
The two are not the same store, so keep these boundaries in mind:
- Channels: the
[[channels]]blocks are written into SQLite only on first start, while the channel table is still empty. From then on the database is the single source of truth: editing the config file neither reseeds nor overwrites console changes. Bulk-adjust existing channels through the console or the admin API instead. - Upstreams:
[[upstreams]]is always read from the configuration file, and the console cannot add or edit them. Saving a channel validates that the referencedupstreamexists in the config, and rejects it otherwise. - Public base URL:
server.public_base_urlis seeded into the settings table on first start, after which the settings table wins and the console can change it. - Outbound routes and EPG sources: seeded the same way, database-authoritative afterwards.
The lite variant creates no database. Its channels come entirely from the configuration file, read-only, with the admin API unavailable.
Enabling and disabling
- Individual channels toggle through the
disabledfield; both the console switch and the admin API write to it. - Bulk operations map to
POST /v1/admin/channels/enable-allandPOST /v1/admin/channels/disable-all, and require thewritescope. - Disabling a channel does not delete its definition or change its position in the list. It does stop the current playback session immediately and removes the channel from public listings.
M3U import and export
Import
Import runs in two passes, preview and apply, both through POST /v1/admin/import/m3u with the apply flag deciding which. The preview returns the planned action and reason for every entry so you can confirm before writing.
Parsing and generation rules:
- Reads the
group-title,tvg-logo,tvg-id, andtvg-nameattributes on#EXTINFlines, plus the trailing title. - The channel
idis slugged fromtvg-id, falling back to the title, then to the URL path. Collisions get-2,-3, and so on, capped at 48 characters. - An address ending in
.mpdis inferred asdash; everything else ashls. - Imported channels always write
source_urland clearupstreamandpath. - Newly created channels default to
on_demandenabled with a 90-second idle timeout. - An entry whose
idalready exists is treated as an update, and only non-empty fields overwrite the existing values. - Entries with an invalid address, or that fail validation, are skipped with the reason recorded in the preview.
- Import never deletes channels that are missing from the list.
- Apply carries the revisions read during the preview, so if anything changed in the meantime the whole batch returns 409 instead of clobbering someone else’s edit.
Export
POST /v1/admin/exports/m3u returns an M3U you can hand straight to a player. It automatically creates an unscoped playback key and builds every address under the /p/{token}/play/ prefix, so an admin credential is never embedded. When the export is no longer needed, revoke that key under Playback Access Control in the console to invalidate the whole file at once.
Self-checks: warmup, probe, and preview
These three are easy to confuse but do entirely different things. All of them require the refresh scope.
probe: test the source, create nothing
POST /v1/admin/channels/{id}/probe runs a single upstream check without creating a session or producing any media. For an HLS channel it fetches the source address, reads a small opening chunk, and returns the status code, content type, final address, and elapsed time. For a DASH channel it parses the manifest, plans tracks once against prefer_height and selection, and verifies that the global keys are in place. The channel editor can also probe an unsaved draft through POST /v1/admin/source-probes. Use it to confirm the address, headers, and route actually work.
warmup: bring the session up early
POST /v1/admin/channels/{id}/warmup starts a session in the background, or simply refreshes the activity timestamp if the channel is already running. It returns 202 immediately without waiting for the first playlist, so success means the startup path has begun, not that media is flowing. Its purpose is removing cold-start latency on on-demand channels.
preview: get a short-lived playback credential
POST /v1/admin/channels/{id}/preview issues a credential valid for five minutes and returns a complete playback URL carrying ?token=, ready for the console’s built-in player. A preview credential can only play; every admin endpoint rejects it, which makes it safe to share briefly with a colleague to confirm the picture.
The usual order is probe to confirm the source is reachable, warmup to confirm a session starts, and preview to confirm the picture is right. If all three pass and something is still wrong, head to Troubleshooting.