Skip to content

Media engine

Choose a media engine, understand native DASH decryption and remuxing support, and configure keys, LL-HLS, and the FFmpeg compatibility fallback.

Updated View as Markdown

The media engine converts a DASH upstream into HLS that players can open directly. The native engine decrypts and remuxes media inside the Kiln process without starting an external program. FFmpeg is used only as a compatibility fallback.

Choosing an engine

The difference between the two paths comes down to this table. Read it first, then pick a value:

Capability Native engine FFmpeg compatibility path
Tracks and bitrates Multi-bitrate ABR, up to 8 video, 16 audio, 16 subtitle tracks One video plus one audio track, a single bitrate
Subtitles TTML converted to WebVTT tracks None produced
LL-HLS Supported Not supported
Decryption cenc and cbcs, multiple KIDs allowed A single CENC key
Output segments CMAF MPEG-TS
External dependency None; everything runs inside the Go process An ffmpeg binary or container

[packager].engine takes three values, and they do not overlap:

Value Behavior
auto Use the native engine when the source supports it; fall back to FFmpeg when it does not and the failure is safe to fall back from
native Native only. An unsupported source fails outright instead of falling back
ffmpeg Skip native planning entirely and always run FFmpeg

A channel’s packager field overrides the global [packager].engine, and auto applies when neither is set. The default an image injects through KILN_DEFAULT_PACKAGER_ENGINE only applies when the config omits [packager].engine — an explicit value always wins.

auto does not always fall back

When native planning fails, auto first decides whether that particular failure can be handed to FFmpeg safely. When it cannot, Kiln returns an error rather than switching engines:

  • A source with more than one KID cannot fall back. The FFmpeg path accepts a single CENC key, so handing it a multi-KID source would decode the wrong picture. The request fails with 502.
  • If a channel selects a subtitle track explicitly (subtitles.mode set to prefer or only), the compatibility engine cannot honor that selection and the request is rejected with 400.
  • If a fallback is needed but FFmpeg is unavailable, the request fails with 503. The Lite and Core images ship without FFmpeg; see Lite, Core & Full.

/readyz moves that boundary earlier: as soon as a DASH channel is pinned to engine = "ffmpeg" while ffmpeg is unavailable, readiness reports 503, so an orchestrator never routes traffic to an instance that is guaranteed to fail.

The native engine

The native engine reads the MPD, plans the tracks, then downloads, decrypts, and repackages segment by segment into CMAF before publishing HLS playlists. The whole chain runs in Go, with no FFmpeg or other external dependency.

What the source has to look like

Dimension Supported by the native engine
Periods Single period only
Segment addressing SegmentTemplate with SegmentTimeline, SegmentTemplate with duration, SegmentList
Video codecs avc1, avc3, hvc1, hev1
Audio codecs mp4a
Subtitle codecs stpp (TTML)
Encryption cenc and cbcs; clear sources also run natively
Track counts Up to 8 video, 16 audio, and 16 subtitle tracks

avc3 and hev1 require their parameter sets to live in avcC or hvcC; a track that carries them in-band only is treated as unsupported. The ABR ladder deduplicates candidates by resolution and frame rate, keeps the highest bitrate in each group, and trims to the top 8 rungs.

How a publication is built

Plan

Fetch and normalize the MPD, then derive the ABR ladder, the audio renditions, and the subtitle tracks from prefer_height and the channel’s track selection.

Verify

Read each track’s init segment, cross-check the manifest’s default_KID against the KID in tenc, and confirm the key catalog has a matching key.

Repackage

Download each segment, decrypt it in memory, and rewrite it as a CMAF segment — sliced into parts when LL-HLS is on.

Publish

Write the multivariant playlist and one media playlist per track, carrying EXT-X-MAP and EXT-X-PROGRAM-DATE-TIME.

Reasons a source is rejected

When planning or verification fails, the logs and the channel’s probe result carry a reason code:

Reason Meaning Falls back under auto
multi_period The manifest declares more than one period Yes
no_video_representation No usable video representation Yes
no_audio_representation No usable audio representation Yes
addressing_unsupported The segment addressing mode is not supported Yes
manifest_codec_unsupported The codec or track shape is outside the native set Yes
manifest_kid_conflicts_with_tenc The manifest’s default_KID disagrees with the init segment’s tenc Yes
missing_key_for_kid The key catalog has no key for that KID Yes
multi_kid_cannot_fall_back The source uses several KIDs, which single-key FFmpeg would decode incorrectly No
native_start_failed Any other error during native start-up Yes

Malformed init segments produce a finer set of reasons — not_fragmented_mp4, multi_track_init, encryption_scheme_unsupported, missing_track_kid, inband_parameter_sets, malformed_media — each meaning exactly what its name says. See Troubleshooting for how to read them.

Key file

Every DASH channel shares one global key catalog, pointed at by [packager].keys_file:

kiln.keystext
# One pair per line; lines starting with # are comments
0123456789abcdef0123456789abcdef:fedcba9876543210fedcba9876543210
01234567-89ab-cdef-0123-456789abcdef:00112233445566778899aabbccddeeff

The parser enforces the format strictly:

  • Each line is one kid:key pair. Blank lines and lines starting with # are ignored.
  • The KID is 32 hex characters and may contain dashes; the key is 32 hex characters and may not.
  • A repeated KID with a different key is an error; a repeated KID with the same key is skipped as a duplicate.
  • A file with no valid entry at all is an error.

Relative paths resolve against the directory holding kiln.toml, not the process working directory — which is why keeping the config and the key file side by side still works under the Windows service.

Keys never appear in any admin API response, and the per-channel keys field has been removed, so there is exactly one place a key can come from.

LL-HLS

With [packager].ll_hls enabled, the native engine publishes low-latency HLS: playlists move to version 9, gain EXT-X-PART-INF and EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES, and three mechanisms come online together.

CMAF parts. Each segment is sliced into parts advertised through EXT-X-PART, with independently decodable parts marked INDEPENDENT=YES. The part currently being produced is announced through EXT-X-PRELOAD-HINT. Part duration comes from part_target_ms, which accepts 100 to 5000 and defaults to 500.

Delta playlists. A player that sends _HLS_skip=YES or _HLS_skip=v2 receives only the tail of the playlist, with the omitted segments counted by EXT-X-SKIP:SKIPPED-SEGMENTS. The skip boundary is six times the target duration, which is exactly what CAN-SKIP-UNTIL advertises.

Blocking reload. A request carrying _HLS_msn, optionally with _HLS_part, is held open until that media sequence number or part is ready. _HLS_part requires _HLS_msn, and a sequence number too far ahead is rejected instead of waiting forever. PART-HOLD-BACK is advertised as twice part_target_ms.

The related playlist parameters:

Key Default Purpose
playlist_size 8 Segments kept in a media playlist
part_target_ms 500 Target duration of a CMAF part, in milliseconds
start_segments 3 Segments published on cold start — the tail of the window for live, the head for on-demand
prefetch_segments 3 Segments prepared concurrently in the pipeline

With ll_hls off, playlists drop back to version 7, no part tags are written, and part_target_ms stops mattering.

Memory versus latency

Peak memory for the native engine is governed mostly by one key: inflight_bytes. It is a byte budget for segment data held across every channel at once, counted in bytes rather than segments because a single 4K segment can be tens of megabytes — a segment-count limit would make memory a function of the upstream bitrate.

What that budget buys is cold-start speed, and nothing else: in steady state each track pulls one segment per refresh and never approaches the limit. The example config records measurements taken with a 4K channel and a 1080p channel running at once, both with several audio renditions:

inflight_bytes Resident memory First 4K playlist
96 MiB (default, 100663296) 140 to 170 MB about 8.6 s
32 MiB (33554432) 106 to 112 MB about 11 s

Lower it when resident memory matters more than how fast a 4K channel starts.

The remaining keys each guard one boundary:

  • max_segment_bytes, default 33554432 (32 MiB), caps a single segment. Together with inflight_bytes it also derives the number of download and decrypt slots, so lowering it tightens concurrency at the same time.
  • primary_track_hold_sec, default 12, is how far in media seconds audio may run ahead of video. It is not an A/V sync knob: it stops audio from sliding its playlist window past what a player stalled on video still needs.
  • stall_timeout_sec, default 180. When the manifest keeps updating but nothing ever reaches the playlist, the fault is on our side, so the publication fails and a restart re-plans from scratch. An unreachable upstream is not this case and keeps retrying. Set -1 to disable.
  • grace_sec, default 30, is how long a segment stays fetchable after it leaves the playlist, covering players that are still reading an older playlist.

The FFmpeg compatibility path

This path is reached only when the native engine cannot handle the source and the failure is safe to fall back from.

[ffmpeg].mode decides how FFmpeg is executed:

  • native runs the local [ffmpeg].binary (default ffmpeg, resolved from PATH).
  • docker has Kiln issue docker run --rm itself against [ffmpeg].docker_image, bind-mounting the work directory, running as a non-root user, and passing outbound proxy environment variables through. No wrapper script is needed.

The rest of the settings:

Key Default Purpose
hls_time 2 Target duration of an output segment, in seconds
hls_list_size 8 Segments kept in the output playlist; the default drops to 4 when low_latency is on
prefer_height unset Resolution ceiling; a channel’s prefer_height wins
low_latency false Picks the default for hls_list_size: 4 when on, 8 when off. An explicit hls_list_size always wins
max_starts 1 Concurrent FFmpeg launches. It does not cover the readiness wait, so one slow source cannot block every other channel’s cold start
log_level error Log level passed to FFmpeg

The compatibility path is noticeably narrower than the native one: it picks one video and one audio rendition, stream-copies with -c copy, emits MPEG-TS segments, and uses a single CENC key. There are no subtitles, no ABR ladder, and no LL-HLS parts. Treat it as a way to keep a channel playable, not as an equivalent implementation.

Subtitles and timed metadata

Subtitles. The native engine consumes DASH stpp tracks, that is, fragmented TTML. Each segment is parsed into cues, clipped to that segment’s time window, and emitted as a WebVTT segment carrying X-TIMESTAMP-MAP, so players see an ordinary HLS subtitle rendition. Language tags are normalized before they are written into EXT-X-MEDIA. Text tracks that are not stpp, or whose addressing is unsupported, are skipped without affecting audio or video; a channel carries at most 16 subtitle tracks. Setting subtitles.mode to off disables subtitles entirely, and prefer or only can only be honored by the native engine.

Timed metadata. emsg boxes inside segments are parsed in both version 0 and version 1, with version 0’s relative time resolved against the segment’s presentation time. Schemes are classified as SCTE-35, ID3, or plain emsg. SCTE-35 splice information is rewritten as an HLS EXT-X-DATERANGE: CLASS is fixed to com.apple.hls.scte35, and SCTE35-OUT, SCTE35-IN, or SCTE35-CMD is written according to direction, alongside PLANNED-DURATION, DURATION, and END-DATE. Observations sharing an event ID are merged across refreshes, so an out seen before a matching in gets its end time filled in automatically. ID3 and other schemes are recognized but are not currently written as playlist tags.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close