Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Adaptive bitrate streaming with HLS, explained for web developers

Disclosure: I work at TPStreams, a video hosting and streaming platform. This post explains the general technique, which applies whichever provider (or none) you use. If you've ever put an MP4 behind a <video> tag and w

Disclosure: I work at TPStreams, a video hosting and streaming platform. This post explains the general technique, which applies whichever provider (or none) you use.

If you've ever put an MP4 behind a <video> tag and watched it stall on a phone over 4G, you've hit the problem adaptive bitrate (ABR) streaming solves. Here's how it works with HLS, the format you'll meet most often.

The problem with a single file

A single MP4 has one resolution and one bitrate. Encode it at 1080p and 5 Mbps and viewers on slow connections buffer; encode it at 480p and everyone on fibre gets a blurry picture. The network a viewer has at minute one is also not the network they have at minute ten.

The idea: many renditions, small segments

ABR fixes this with two moves:

  1. Encode several renditions of the same video, for example 240p, 360p, 480p, 720p and 1080p, each at a different bitrate.
  2. Cut each rendition into short segments (typically 2 to 6 seconds) aligned at the same timestamps, so the player can switch rendition at any segment boundary.

The player measures how fast segments are downloading and picks the best rendition the connection can sustain, switching up or down as conditions change.

What HLS looks like on disk

HLS (HTTP Live Streaming) describes all this with plain-text playlists (.m3u8).

The multivariant (master) playlist lists the renditions:

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.4d401e,mp4a.40.2"
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=854x480,CODECS="avc1.4d401f,mp4a.40.2"
480p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8

Each media playlist lists that rendition's segments:

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="init.mp4"
#EXTINF:6.0,
seg_0001.m4s
#EXTINF:6.0,
seg_0002.m4s
#EXT-X-ENDLIST

Everything is served over ordinary HTTP, so any CDN can cache it. That is a big reason HLS won: no special streaming server is needed.

Producing renditions yourself with FFmpeg

A minimal two-rendition ladder:

ffmpeg -i input.mp4 \
  -filter_complex "[0:v]split=2[v1][v2];[v1]scale=-2:720[v720];[v2]scale=-2:360[v360]" \
  -map "[v720]" -c:v:0 libx264 -b:v:0 2800k -maxrate:v:0 3000k -bufsize:v:0 5600k \
  -map "[v360]" -c:v:1 libx264 -b:v:1 800k -maxrate:v:1 900k -bufsize:v:1 1600k \
  -map a:0 -map a:0 -c:a aac -b:a 128k \
  -g 48 -keyint_min 48 -sc_threshold 0 \
  -f hls -hls_time 6 -hls_playlist_type vod \
  -hls_segment_type fmp4 \
  -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0 v:1,a:1" \
  out/%v/index.m3u8

The flags that matter most for ABR:

  • -g, -keyint_min and -sc_threshold 0 give a fixed keyframe interval (2 seconds at 24 fps here), so every rendition has keyframes at the same points. Without aligned keyframes, switching causes visible glitches.
  • -maxrate and -bufsize cap bitrate spikes, so the BANDWIDTH value the player relies on is honest.
  • -hls_time sets segment length. Shorter segments react faster to network changes but mean more requests.

Playing it in the browser

Safari plays HLS natively. Elsewhere, use a library such as hls.js:

<video id="v" controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
  const video = document.getElementById('v');
  const src = '/out/master.m3u8';
  if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = src;
  } else if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(src);
    hls.attachMedia(video);
    hls.on(Hls.Events.LEVEL_SWITCHED, (_, d) =>
      console.log('Now playing level', d.level));
  }
</script>

Open DevTools, go to the Network tab, throttle to "Slow 4G", and you'll see the player drop to the 360p segments, then climb back up when you remove the throttle.

Common mistakes

  • Unaligned keyframes across renditions (see above).
  • A ladder that's too sparse. Jumping straight from 360p to 1080p makes every switch obvious. Add steps in between.
  • No low rung. A 240p rendition at a few hundred kbps keeps mobile viewers watching instead of buffering.
  • Forgetting CORS on the CDN when the player page and the segments live on different domains.
  • Treating HLS as security. Splitting a video into segments does not stop downloading; tools reassemble HLS easily. Protection needs encryption, DRM (Widevine and FairPlay) and access tokens, which is a separate topic.

Build or buy?

Running FFmpeg yourself is fine for a handful of videos. At scale you'll also need a job queue, storage, a CDN, captions, analytics and possibly DRM. Managed services such as Mux, Cloudflare Stream, Bunny Stream, Gumlet, VdoCipher, AWS Elemental MediaConvert and TPStreams generate the rendition ladder for you; compare them on output formats, DRM support, pricing model and where your viewers are.

If you want to see how we handle multi-rendition transcoding (240p to 4K, HLS and DASH), it's described here: https://tpstreams.com/transcoding-video/

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.