VOD Docs

HLS output

The bitrate ladder, segment layout and playlists the pipeline produces, and how players use them.

HLS (HTTP Live Streaming) delivers video as many small segment files listed in text playlists (.m3u8). A player downloads the master playlist, picks a rendition based on measured bandwidth and screen size, and fetches that rendition's segments one by one, switching up or down between segments as conditions change. That is adaptive bitrate (ABR) streaming.

Animation · placeholder
ABR in action: bandwidth drops, the player steps down from 1080p to 480p at the next segment boundary, then climbs back

Timeline animation with a bandwidth graph above and the chosen rendition below.

The bitrate ladder

Defined as QUALITY_SETTINGS in backend/app/core/config.py:

RenditionResolutionVideo bitrateStatus
2160p3840 × 216020,000 kbpsDefined, disabled in the workflow
1440p2560 × 144010,000 kbpsActive
1080p1920 × 10805,000 kbpsActive
720p1280 × 7202,500 kbpsActive
480p854 × 4801,000 kbpsActive
360p640 × 360500 kbpsActive
240p426 × 240300 kbpsActive
144p256 × 144200 kbpsActive

Every rendition also has 128 kbps AAC audio.

Which renditions a video gets

  • No upscaling: a rendition is skipped when its height is greater than the source height. A 1080p source gets 1080p and below (six renditions); a 720p source gets five.
  • Fixed frame size: every rendition is scaled to fit inside its box and padded with black to the exact width and height, so a vertical or ultra-wide source keeps its shape inside a 16:9 frame.
  • The list actually produced is stored in available_qualities.
Chart · placeholder
Ladder chart: resolution vs bitrate for the seven active renditions

Bar or step chart; optionally overlay measured file sizes per rendition for a sample video.

Output layout

vod-processed/<video_id>/segments/
├── master.m3u8
├── 1080p/
│   ├── playlist.m3u8
│   ├── segment_0000.ts
│   ├── segment_0001.ts
│   └── …
├── 720p/
└── …

Segments are MPEG-TS (.ts) files, numbered with four digits.

Playlists

Written by create_manifest. One EXT-X-STREAM-INF entry per rendition, highest first, with BANDWIDTH (bits per second, from the configured bitrate) and RESOLUTION:

#EXTM3U
#EXT-X-VERSION:3

#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/playlist.m3u8

Why segment lengths vary

The target is 6 seconds, but the playlist above has segments of 8.5 s and 4.4 s. Segmenting uses -c copy, which cannot create keyframes, so FFmpeg can only cut where the encoder happened to put one. The transcode step does not force keyframes at fixed intervals, so each rendition's keyframes, and therefore its segment boundaries, land in different places.

This still plays correctly, but it is not ideal for ABR: when renditions' segments are aligned, a player can switch quality at any boundary cleanly. Forcing a keyframe every 6 seconds during transcoding (for example -force_key_frames "expr:gte(t,n_forced*6)" -sc_threshold 0) would give aligned, exactly 6-second segments. See Improvements.

Delivery

Playlists and segments are served by Caddy from MinIO under /storage/:

FileCache
.ts segmentspublic, max-age=31536000, immutable
.m3u8 playlistspublic, max-age=60

Segments never change once written, so they can be cached for a year by browsers and any CDN in front.

In the player

The frontend uses video.js with an hls.js-backed element. It caps the chosen quality to the player's size, starts with a 1 Mbps bandwidth estimate, and lets the viewer override the automatic choice from a quality menu. Details: Player.

On this page