HLS ingest¶
Sapsan follows someone else's HLS playlist in client (pull) mode: it polls the playlist, fetches the segments and feeds frames into the pipeline just like RTSP, SRT or MPEG-TS. That is how almost every aggregator, every third-party CDN and every partner who is not allowed to open UDP or SRT delivers a stream.
Configuration¶
streams:
- name: chan1
inputs:
- hls:
url: https://cdn.example.com/live/index.m3u8
| Parameter | Default | Description |
|---|---|---|
url |
— | playlist address exactly as the operator wrote it |
headers |
none | headers added to every request to the source: playlist, segments, keys |
variant_mode |
single |
single — one variant of the master, ladder — the whole ladder |
variant_bandwidth |
none | which bitrate to match the variant against (nearest below); without it the highest BANDWIDTH wins |
skew_threshold_ms |
500 |
the skew at which the ladder is declared out of sync |
skew_policy |
report |
what to do on a skewed verdict: report — keep going and show it, fallback_to_single — fall back to a single variant |
connect_timeout_ms |
5000 |
connection timeout |
read_timeout_ms |
15000 |
body timeout: playlist, segment, key |
max_body_bytes |
64 MiB | size limit of a single response |
peer_timeout_ms |
global | how long to wait for frames before the source is considered lost |
An omitted field means the server default, not "disabled".
The scheme can be written in three equivalent ways:
https://host/path.m3u8— recognized by the path extension, a query string does not get in the way;hls://host/path.m3u8— same ashttp://;hlss://host/path.m3u8— same ashttps://.
Adding the source in the console¶
Open the stream, go to the Sources tab, press + Add Source and pick the HLS protocol. Only one field is required: URL, the playlist address.

The rest is optional, and an empty field means the server default: connect, body and peer timeouts, the body size limit, the variant bitrate.
If the source requires a token, press Add header and fill in the name, then the value. The row becomes a real header the moment it gets a name: a header without a name is rejected by the server and never goes on the wire.

The Capture the whole ladder checkbox reveals Skew threshold and the When renditions are skewed choice. It is a separate checkbox rather than a consequence of the source serving a master playlist: capturing the ladder multiplies both the ingest traffic and the number of tracks in the stream, and on a stream with an archive it shortens the recording depth by the same factor.

Do not forget to Save: the input reaches the server only on that button.
From then on the state of the source lives on the same tab. The card in the Source health block carries the ladder verdict and the worst skew:

The per-rendition breakdown — declared and actual skew, drift, unpaired segments and keyframe divergence — sits below it in the Rendition skew block:

Start rule¶
Whatever was already in the playlist at connect time is history. Capture starts from the first segment that appears after the connection, and it behaves the same way on a reconnect.
There is deliberately no start-depth setting: otherwise every dropped connection would make the stream flush the accumulated window into the pipeline in seconds instead of broadcasting.
Authentication¶
Two ways, and they combine:
- headers —
headers, e.g.Authorization: Bearer <token>; they go with the playlist, variants, init segments, segments and keys of the same origin, and they never follow a redirect to a foreign origin; - credentials in the URL itself — plain Basic authentication done by the HTTP client.
Header values never reach the log: only their names are printed. The input address is logged in full and returned as is by the management API — only the operator knows which part of that address is the secret, and guessing would either leave the token in the query or cut away the very part the input is recognized by in the log.
Ladder and rendition skew¶
With variant_mode: ladder every variant of the master is captured. The highest BANDWIDTH variant becomes the reference, and the rest are compared against it segment by segment, by media sequence number. The verdict is exposed in the input statistics, field hls_ladder:
synced— the renditions run together, switching between them is safe;skewed— the skew exceeded the threshold or the structure is broken: switching will jerk or desynchronize;unmeasurable— nothing to compare, no segment pairs share a number.
Skew is measured two ways at once. Declared is the difference of EXT-X-PROGRAM-DATE-TIME in the markup, actual is the difference of the media time of the first frames. When the two disagree, it is the markup that is ill, not the stream, and it is the source owner who fixes it.
Note
Sapsan does not bring ladder variants onto a common timescale, and it does not repair bad content at all: a silent fix does not make a ladder switchable, it only hides the defect. So the skew is shown as a number and the operator decides.
The fallback_to_single policy drops the capture to a single variant on a skewed verdict — for some sources that is the only workable mode. The fallback lasts until the input is restarted.
Encryption¶
AES-128 (whole segment) and SAMPLE-AES (per frame, for H.264 and AAC) are supported. The key is fetched by the same client with the same headers as everything else and cached by address, so key rotation in the middle of a playlist works by itself — a new key has a new address.
A silent key server is an input error naming the key address and the response code; the segment body is not downloaded at all, since there would be nothing to decrypt it with.
CENC/DRM (Widevine, PlayReady, FairPlay) is not supported and is rejected by the KEYFORMAT name.
What is supported¶
| Feature | State |
|---|---|
Media playlist: EXT-X-MEDIA-SEQUENCE, EXTINF, EXT-X-ENDLIST |
yes |
Master playlist: variant selection, EXT-X-MEDIA renditions |
yes |
fMP4 (EXT-X-MAP) and MPEG-TS segments |
yes |
EXT-X-BYTERANGE |
yes |
EXT-X-DISCONTINUITY, playlist recreation, falling behind the window |
yes |
EXT-X-GAP |
yes, skipped without an error |
EXT-X-PROGRAM-DATE-TIME — media time anchored to UTC |
yes |
AES-128 and SAMPLE-AES (H.264, AAC) encryption |
yes |
| Ladder: capture of every variant, skew measurement | yes |
LL-HLS: EXT-X-PART, blocking reload |
no, parts are ignored |
| CENC/DRM | no |
| Packed audio: bare ADTS or ID3+AAC instead of a container | no, the format is named in the error |
| TTML subtitles and WebVTT segments | no, the track is recognized and skipped |
| MPEG-DASH ingest | no |
Applying changes¶
Only a change of url, headers or the HTTP settings (connect_timeout_ms, read_timeout_ms, max_body_bytes) restarts the input: a new source has its own track numbering, its own init and its own position in the playlist. Everything else — variant mode, skew threshold and policy, peer_timeout_ms — applies in place, without interrupting the broadcast.
Verification¶
Open http://server/streaming/v/chan1/index.m3u8 in a player or request a screenshot at http://server/streaming/live-preview-jpeg/chan1.
Look beyond "frames are flowing" at three things:
- how far the output lags behind the playlist edge — without it a "the stream is behind" complaint is indistinguishable from "the source is behind", and the two are fixed in different places;
- skipped segments and their breakdown: washed out of the window, cancelled in flight, declared with
EXT-X-GAP. The last one is not a source error but its honest statement; - the seam between neighbouring fragments — a silent defect: every segment was downloaded, no parse errors, yet the media inside them does not join up, and the archive keeps the hole.