SevyDevy

Guide · Frontend

Design a Video Player Frontend (HLS and Adaptive Bitrate)

25 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • A video streaming frontend is the browser player that fetches a video in short pieces and picks, for each piece, a quality the network can carry now.
  • HLS (HTTP Live Streaming) lists those pieces in plain text playlists. A master playlist lists every quality, called a rendition, and each rendition's media playlist lists its short video files, called segments.
  • Adaptive bitrate (ABR) logic picks the quality. It uses two signals: recent download speed, and the buffer, meaning the seconds of video downloaded but not yet played.
  • Browsers without built-in HLS need Media Source Extensions (MSE), a browser API that lets JavaScript feed downloaded segments to the video element. Libraries such as hls.js do this for you.
  • Beginners can stop after the player's parts. Engineers then get what hls.js 1.7.3 does by default: up to 600 seconds of buffer on low-quality levels, dropped audio-only renditions, and a size cap that counts screen pixel density.

What goes wrong when a video plays on a slow mobile connection?

The video stalls, because the player asks for more data per second than the network can deliver. The spinning circle appears whenever the downloaded video runs out before the next piece arrives.

Picture a common setup. A small streaming app serves folk music and regional films to viewers in small towns of Bihar, mostly on low-cost phones with a weak mobile signal. The setup is made up, but the problem is real.

The app's first video streaming frontend was simple. Each film was one 720p MP4 file (a common video file format), where 720p means a picture 720 pixels tall. The page pointed a video element straight at that file. On office Wi-Fi in Patna, it looked perfect.

Then the team's QA engineer took four test phones to a weekly market outside the city and played the same film on each. Her notes (made up) were blunt:

  • The film took 14 seconds to show its first frame.
  • It stopped to load 9 times in the first 5 minutes.
  • When the signal dipped near a tower shadow, the video froze for 40 seconds.

The cause was arithmetic. The file needed about 2,500 kilobits (2.5 megabits) every second, and the network often carried under 800. No setting on one fixed file could close that gap.

The rest of this guide follows the team as it rebuilds the player so the same film keeps playing on that connection.

What words do you need before building a video player?

You need six words: bitrate, rendition, segment, playlist, buffer and stall. Each one gets a plain meaning and an example from the team's app.

Bitrate. Bitrate is how much data one second of video needs, usually in kilobits per second (kbps), where a kilobit is 1,000 bits. The team's 720p version needs about 2,500 kbps, and its 240p version about 300 kbps.

Rendition. A rendition is one full copy of the video at one picture size and bitrate. The set of all renditions, from smallest to largest, is the bitrate ladder. Players often call each rendition a level.

Segment. A segment is a short piece of one rendition, saved as its own file. The team cuts every rendition into 4 second segments at the same points in the film, so the pieces line up across qualities.

Playlist. A playlist is a small text file that lists, in order, what to fetch: other playlists or segment files. The player reads it to know what to download next.

Buffer and stall. The buffer is video already downloaded but not yet played. A stall, also called rebuffering, is when playback stops because the buffer is empty.

How does HLS streaming work in a web player?

HLS cuts a video into short segments at several qualities and lists them in text playlists. The player reads the playlists, then downloads segments one by one over ordinary HTTP, the same protocol that loads web pages.

HLS stands for HTTP Live Streaming. It is written down in RFC 8216, published in August 2017. RFCs are the numbered documents that also hold most internet standards, though this one is labelled Informational. It covers films as well as live events.

There are two kinds of playlist. A master playlist lists media playlists, one per rendition, and the RFC calls each rendition a variant stream. A newer draft revision (version 22, dated 1 May 2026, as of 19 October 2026) calls the master playlist the multivariant playlist, so expect both names.

Each variant has an EXT-X-STREAM-INF line with a BANDWIDTH value. The RFC defines BANDWIDTH as the peak segment bit rate of that variant, in bits per second. An optional AVERAGE-BANDWIDTH gives the average.

Here is a cut-down master playlist for the team's film, with three of its five variants. CODECS names the video and audio formats; the RFC says every variant line should include it.

text
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=80000,CODECS="mp4a.40.2"
audio/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=360000,AVERAGE-BANDWIDTH=300000,RESOLUTION=426x240,CODECS="avc1.42e01e,mp4a.40.2"
240p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,AVERAGE-BANDWIDTH=2500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8

What just happened? Each line starting with EXT-X-STREAM-INF describes one variant, and the line after it says where that variant's media playlist lives. The 240p peak of 360,000 is above its 300,000 average, as a peak should be.

Each media playlist lists one variant's segments in order. Its EXT-X-TARGETDURATION tag gives the maximum segment duration in seconds. The RFC calls 10 seconds a typical target duration; the team uses 4. EXT-X-ENDLIST says no more segments will be added.

text
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:4.0,
seg00001.ts
#EXTINF:4.0,
seg00002.ts
#EXT-X-ENDLIST

You can check both files. The playlist parser in hls.js 1.7.3 read the master playlist above as three variants with BANDWIDTH 80,000, 360,000 and 3,000,000. It read the media playlist as two segments, a 4 second target and 8 seconds in total.

To inspect a real stream, run curl followed by its master playlist URL. The output is plain text starting with EXTM3U, then one EXT-X-STREAM-INF line per variant.

In the diagram, follow the files: one small text file leads to the media playlists, and those lead to hundreds of short video files. A CDN (content delivery network, servers spread across many cities) serves them all.

How the team's HLS files fit together

Notice the order: the player reads small text files first and only then fetches video. Every variant is cut at the same 4 second marks.

Diagram connections:

  • Web player to CDN (HTTP GET for each file)
  • CDN to Master playlist (Serves first)
  • Master playlist to 240p media playlist (Lists)
  • Master playlist to 720p media playlist (Lists)
  • 240p media playlist to Segment files (Points to)
  • 720p media playlist to Segment files (Points to)
  • Web player: Runs in the viewer's browser
  • CDN: Serves every playlist and segment
  • Master playlist: Lists every variant with its BANDWIDTH
  • 240p media playlist: Lists the 240p segments in order
  • 720p media playlist: Lists the 720p segments in order
  • Segment files: 4 second pieces of every variant

For a live event, such as a festival stream, the media playlist keeps growing. The RFC says the client reloads the playlist to discover added segments. For a film, the list is complete from the start.

What is adaptive bitrate streaming in a video player?

Adaptive bitrate streaming means the player switches between renditions while the video plays, picking for each next segment the best quality the network can carry. Quality drops before the video stops.

Picture someone drawing water from a village well. When the rope pulls easily, she uses the big brass pot. When the rope grows heavy, she switches to the small pot.

The small pot means less water each trip, but water keeps reaching the house and nobody goes thirsty. She goes back to the big pot only when the pulls feel easy again.

The rope is the network. The big pot is a 720p segment, and the small pot is a 240p segment. Choosing the pot before each pull is what ABR logic does before each download.

Where the picture breaks: she feels the rope's weight while pulling, but a player learns the speed only after a segment has arrived. Every choice is a guess based on the past.

The RFC says clients should switch between variant streams to adapt to network conditions. It also says the switching algorithms are beyond its scope. That choice is the player's job, and it is the heart of this design.

Here is the bitrate ladder the team built with its encoding team. Notice the audio-only rung at the bottom: for folk music, sound matters more than picture.

The team's bitrate ladder, with sizes per 4 second segment (made up)
RenditionAverage bitrateOne segmentGood for
Audio only64 kbps32 KBVery weak signal, music videos
240p300 kbps150 KBSlow mobile link
360p600 kbps300 KBFair mobile link
480p1,000 kbps500 KBGood 4G on a phone
720p2,500 kbps1.25 MBWi-Fi or strong 4G

The segment sizes come from simple maths. A 600 kbps segment lasting 4 seconds holds 2,400 kilobits. Divide by 8 to get kilobytes, and you get 300 KB.

How do you design a video streaming frontend?

You design a video streaming frontend as a few parts with one job each: a segment fetcher, an ABR controller, a buffer, the video element and an analytics reporter. A player engine connects them.

Follow the arrows from the viewer's tap to the analytics box.

Parts of the team's web player

Notice that the ABR controller never downloads anything. It reads measurements and recommends a level; the engine acts on it.

Diagram connections:

  • Player controls to Player engine (Play, seek, quality)
  • Player engine to Segment fetcher (Next segment and level)
  • Segment fetcher to CDN (HTTP GET)
  • Segment fetcher to ABR controller (Bytes and download time)
  • ABR controller to Player engine (Recommended level)
  • Player engine to MSE SourceBuffer (appendBuffer)
  • MSE SourceBuffer to Video element (Media to play)
  • Video element to Playback analytics (Waiting and playing events)
  • Player controls: Play, pause, seek and quality menu
  • Player engine: Decides what to fetch next and when
  • Segment fetcher: Downloads playlists and segments
  • CDN: Edge servers close to the viewers
  • ABR controller: Picks a level from speed and buffer
  • MSE SourceBuffer: Holds downloaded media for playback
  • Video element: Decodes and shows the frames
  • Playback analytics: Startup time, stalls, switches

Where Media Source Extensions fit

Safari has built-in HLS, so a playlist URL can be the video element's source. The hls.js README notes that Chrome now also reports HLS support, but warns that Chrome 147 may fail on some streams natively.

So the README recommends trying hls.js first, with its Hls.isSupported() check, and native playback only as the fallback. Where hls.js runs, it needs Media Source Extensions.

Media Source Extensions (MSE) is a browser API from the W3C, the web standards body, that lets JavaScript generate the media stream a video element plays. As of 19 October 2026, its version 2 is a Working Draft dated 7 August 2026. Your code creates a MediaSource, adds a SourceBuffer for one media type, and appends downloaded bytes into it.

Most teams do not write this layer from scratch. hls.js, an open source library, reads HLS playlists, fetches segments and feeds MSE for you. On iPhones with iOS 17.1 or later, it uses Managed Media Source (MMS) instead of plain MSE. MMS is a power-saving variant of MSE, where the browser itself can clean up buffered video.

How does a video player choose which quality to play?

A video player compares each rendition's bitrate with two signals: how fast recent segments downloaded and how many seconds of video are already buffered. It picks the highest rendition that fits safely.

Throughput-based choice

Throughput is the download speed the player actually saw. After each segment, it divides the bits received by the time taken. Single samples jump around, so players smooth them into a moving average.

hls.js 1.7.3 keeps two exponentially weighted moving averages, where each new sample moves the estimate only part of the way. For on-demand video, half the fast one comes from the last 3 seconds of download time, and half the slow one from the last 9.

It uses whichever average is lower. Its source comment says this makes it adapt down quickly but up more slowly. Before any download, it starts from the abrEwmaDefaultEstimate setting, 500,000 bits per second by default.

If you leave that setting unset, hls.js 1.7.3 raises the starting estimate to the BANDWIDTH of the first video variant in the master playlist, up to 5,000,000. The startup section shows why that order matters.

Its docs describe two more default margins. To stay on or drop to a level, its bitrate must fit under 95 percent of the estimate. To switch up, it must fit under 70 percent.

That gap is deliberate. Going down should be quick, because a stall hurts more than a blurry picture. Going up should be slow, because a mobile network in a moving bus can change within a minute.

Buffer-based choice

Buffer-based ABR picks quality mainly from how full the buffer is. A big buffer can survive one slow segment, so it allows higher quality. A nearly empty buffer forces the lowest safe level.

A 2014 paper at SIGCOMM, a networking research conference, tested this idea at Netflix, in experiments spanning millions of real users. Choosing the rate from the buffer cut the rebuffer rate by 10 to 20 percent against Netflix's then-default algorithm.

It did so at a similar average video rate. The authors also found that a simple speed estimate still matters at startup, while the buffer is growing from empty.

BOLA, published in 2016, picks the bitrate from the buffer level and, its authors write, needs no bandwidth prediction. An updated version ships in dash.js, the reference player for MPEG-DASH.

In dash.js 5.2.1, the latest release as of 19 October 2026, the BOLA rule is switched on by default next to a throughput rule. hls.js mixes signals too: it weighs expected download time against the time left before the buffer runs out.

The team's mix of both

The team uses both signals. The download speed sets the upper limit, and the buffer decides how much of that limit the player dares to use. Follow the loop in the diagram from top to bottom, then back to the start.

Choosing the level for the next segment

This runs once per segment. Notice the decision box: a low buffer makes the player spend only 60 percent of its speed estimate.

Diagram connections:

  • Segment downloaded to Update speed estimate
  • Update speed estimate to Buffer under 10 s?
  • Buffer under 10 s? to Use 60% of the estimate (Yes)
  • Buffer under 10 s? to Use 85% of the estimate (No)
  • Use 60% of the estimate to Highest level that fits
  • Use 85% of the estimate to Highest level that fits
  • Highest level that fits to Fetch next segment
  • Fetch next segment to Segment downloaded (Next round)
  • Segment downloaded: Bytes and milliseconds are known
  • Update speed estimate: Weighted average of recent samples
  • Buffer under 10 s?: Seconds of video ready to play
  • Use 60% of the estimate: Leaves room for a slow download
  • Use 85% of the estimate: Buffer can absorb a dip
  • Highest level that fits: Bitrate must be under the budget
  • Fetch next segment: At the chosen level

Here is that loop in TypeScript, with a check at the end. The player calls recordDownload after every segment and pickLevel before the next one. Levels are sorted from lowest to highest bitrate.

typescript
type Level = { name: string; bitrate: number }; // bitrate in bits per second

let estimate = 500_000; // first guess before any download is measured
const WEIGHT = 0.3;     // how much the newest download counts

function recordDownload(bytes: number, ms: number): void {
  const sample = (bytes * 8 * 1000) / ms; // bits per second for this segment
  estimate = WEIGHT * sample + (1 - WEIGHT) * estimate;
}

function pickLevel(levels: Level[], bufferSec: number): number {
  const safety = bufferSec < 10 ? 0.6 : 0.85; // be careful when the buffer is low
  let choice = 0;                              // levels are sorted low to high
  levels.forEach((lvl, i) => {
    if (lvl.bitrate <= estimate * safety) choice = i;
  });
  return choice;
}

// Check it with the team's ladder and one 4 second 240p segment of 150,000 bytes
const ladder: Level[] = [
  { name: "audio only", bitrate: 64_000 },
  { name: "240p", bitrate: 300_000 },
  { name: "360p", bitrate: 600_000 },
  { name: "480p", bitrate: 1_000_000 },
  { name: "720p", bitrate: 2_500_000 },
];
recordDownload(150_000, 1_000); // the segment took 1 second, a 1.2 Mbps sample
console.log("estimate:", Math.round(estimate), "bits per second");
console.log("buffer 6 s ->", ladder[pickLevel(ladder, 6)].name);
console.log("buffer 25 s ->", ladder[pickLevel(ladder, 25)].name);

Compiled with TypeScript 5.9.3 and run on Node 22, it prints:

text
estimate: 710000 bits per second
buffer 6 s -> 240p
buffer 25 s -> 360p

What just happened? The 150,000 byte segment arrived in 1 second, a sample of 1,200,000 bits per second. The estimate kept 70 percent of the old 500,000 and took 30 percent of the sample, giving 710,000.

With 6 seconds of buffer, the player spends 60 percent of that, about 426,000, so 240p fits and 360p does not. With 25 seconds, it spends 85 percent, about 603,500, so 360p fits.

How much video should a player buffer ahead?

A player should buffer enough to ride out the common network dips its viewers see, but no more. Every extra second is data a viewer pays for even if they quit early. For the team, that is about 30 seconds.

It is easy to read the hls.js defaults as 30 seconds ahead. That is the maxBufferLength setting, and the hls.js docs call it the guaranteed length it tries to reach. It is a floor, not a ceiling.

In hls.js 1.7.3, the real goal is the larger of maxBufferLength and the time needed to fill maxBufferSize, 60 MB by default, at the level's peak bitrate. A third setting, maxMaxBufferLength, caps it at 600 seconds by default.

Pause and predict how far ahead hls.js fetches on the 240p level with default settings. Then check the table, which uses the peak bitrates from the team's playlists.

Forward buffer goal in hls.js 1.7.3 for the team's video levels, from its own getMaxBufferLength function run in Node 22
LevelPeak bitrate (BANDWIDTH)Default settingsWith maxMaxBufferLength 30
240p360 kbps600 s30 s
360p720 kbps600 s30 s
480p1,200 kbps400 s30 s
720p3,000 kbps160 s30 s

So a viewer on 240p can have up to 10 minutes of film downloaded ahead. At its 300 kbps average that is 22.5 MB, bought on prepaid data even if the viewer quits after two minutes.

The team sets the ceiling as well as the floor:

typescript
import Hls from "hls.js";

const hls = new Hls({
  maxBufferLength: 30,    // floor: try to keep 30 s ready ahead
  maxMaxBufferLength: 30, // ceiling: never fetch more than 30 s ahead
  backBufferLength: 10,   // keep only 10 s of video already played
  capLevelToPlayerSize: true,
  maxDevicePixelRatio: 1, // size cap counts layout pixels, not screen pixels
});

This config type-checks against the hls.js 1.7.3 types. Run through the same function, it gives a goal of 30 seconds at both the 240p and 720p peaks.

To check it on a phone, open the browser console during playback. hls.mainForwardBufferInfo.len shows the seconds actually buffered ahead, and should level off near 30.

hls.maxBufferLength shows the current goal, 30 here. That getter exists in the 1.7.3 code and types but is undocumented as of 19 October 2026, so pin your version.

When the browser says the buffer is full

Browsers also cap how much media a SourceBuffer can hold. MDN, Mozilla's web developer reference, calls this a browser-defined limit. When an append would pass it, appendBuffer throws a QuotaExceededError.

The fix is to remove video the viewer has already watched, then try again. The function below does that, keeping the last 10 seconds so a quick rewind works without a download.

typescript
function appendSegment(sb: SourceBuffer, video: HTMLVideoElement, data: ArrayBuffer): boolean {
  try {
    sb.appendBuffer(data); // caller waits for "updateend" before the next append
    return true;
  } catch (e) {
    if (e instanceof DOMException && e.name === "QuotaExceededError") {
      const keepFrom = video.currentTime - 10; // keep the last 10 s for small rewinds
      if (keepFrom > 0 && !sb.updating) sb.remove(0, keepFrom);
      return false; // try the same segment again after the remove finishes
    }
    throw e;
  }
}

The catch block checks the error by name, because other errors need different handling. The remove call deletes everything from the start up to 10 seconds behind the playhead, the current playing position.

We ran this function against a fake SourceBuffer in Node 22. With the playhead at 95 seconds, a quota error led to remove(0, 85) and a false return. Other errors were thrown again.

At 6 seconds, there was nothing old enough to remove, so it returned false without removing. That case matters: if forward video alone fills the quota, a retry fails again. The player must pause fetching until playback frees space.

Two rules apply here. Wait for the updateend event, which fires when an append or remove finishes, before the next append. And never call remove while updating is true, or it throws an InvalidStateError.

hls.js 1.7.3 handles all of this itself. On a first QuotaExceededError it evicts the minimum back buffer and retries. If nothing can be evicted, it reports BUFFER_FULL_ERROR and, while the playhead is buffered, lowers its maximum buffer length.

How do you make a video start playing fast on a slow connection?

Fetch a low quality first segment, keep the first download small and show a picture straight away. The player can step up once the buffer grows.

Startup is the hardest moment. The buffer is empty, and the player has no speed measurements yet. The 2014 paper found exactly this: speed estimates matter most while the buffer grows from empty.

The team's startup rules are short:

  • Start at 240p on mobile, unless the last session on this phone measured a much faster network.
  • Begin playback after the first 4 second segment arrives, not after 30 seconds of buffer.
  • Show the film's poster image at once, so the screen is never blank while the first segment loads.
  • Fetch the master playlist while the viewer is still on the film's details page.
  • Cap quality to the player's real size, so a small phone screen does not fetch 720p.

The last rule maps to the hls.js setting capLevelToPlayerSize, which is off by default. When on, automatic selection stops at the smallest level that covers the video element's size. There is a catch.

hls.js multiplies the element's size by devicePixelRatio, the number of physical screen pixels per layout pixel. The docs say this happens on some devices and suggest ignoreDevicePixelRatio for a strict limit.

In the 1.7.3 source, it happens on every device unless ignoreDevicePixelRatio is true. We ran that sizing function for a 360 by 203 pixel player. The cap was 240p at a pixel ratio of 1, 480p at 2 and 720p at 3.

To see a phone's ratio, type window.devicePixelRatio in its browser console through remote debugging. If data matters more than sharpness, set maxDevicePixelRatio to 1, as in the config above.

Another setting, testBandwidth, downloads one lowest-level segment to measure speed before choosing the first automatic level. Its docs say it has an effect only when startLevel is set to -1.

Playlist order matters for the first startup rule. With default settings, hls.js 1.7.3 picks its start level from the starting estimate described earlier. In our run, listing 720p first gave a 3,000,000 estimate and a 720p start; the team's order gave 240p.

So the team lists 240p as its first video variant. Setting startLevel to 0, the lowest level, also gave a 240p start in our run.

What happens when the viewer picks a quality by hand

Some viewers want a fixed quality, for example 240p to save data. hls.js 1.7.3 offers three setters for a lasting manual choice, and only one of them never flushes the buffer.

  • currentLevel switches at once. It aborts the current download and flushes the whole buffer, so playback rebuffers.
  • nextLevel switches as soon as possible. Its source comment says it may abort loading and flush buffered video after the playing segment.
  • loadLevel applies from the next segment loaded and never flushes. Its source comment says it may interrupt the download in progress, and the new quality appears after the existing buffer.

On a slow link, a flushed buffer means a fresh stall. So the team's quality menu sets loadLevel. A fourth setter, nextLoadLevel, also never flushes, but its docs say it forces the quality for the next segment only.

Audio only needs its own route. When video variants exist, hls.js 1.7.3 removes audio-only variants. Loaded with the team's master playlist, it kept 240p and 720p and dropped audio only.

So the menu's Audio only option loads a separate audio-only master playlist instead.

How do you test video playback on a slow network?

Replay real network speed patterns in the lab, then check the same metrics on real phones in the field. Lab tests catch regressions, and field data shows what viewers actually get.

The QA engineer built her test plan around four numbers that viewers feel:

  • Startup time: seconds from tapping play to the first frame.
  • Rebuffer ratio: seconds spent stalled divided by seconds watched.
  • Average bitrate: the mean quality actually played, not the quality requested.
  • Switch count: how often quality changed per 10 minutes of watching.

The video element helps here. It fires a waiting event when playback stops for a temporary lack of data, and a playing event when it starts again. The time between them is one stall.

  1. Record real network traces

    Log download speeds on test phones in markets, buses and homes. Each trace is a list of speeds, one per second, for 20 minutes.

  2. Replay traces in the lab

    A throttling proxy (a tool that slows the network on purpose) plays each trace back while the player streams a film. The same traces run against every new build.

  3. Compare against the last release

    If rebuffer ratio or startup time gets worse on any trace, the build fails. A higher average bitrate never excuses more stalls.

  4. Test on cheap phones

    Run the same traces on a low-memory phone. That is where quota errors and slow decoding show up first.

  5. Watch field metrics after release

    The analytics part sends the four numbers from real sessions. Compare slow-network sessions with Wi-Fi sessions every week.

After the rebuild, the QA engineer went back to the same market. The film showed its first frame in about 3 seconds, and stalls in the first minute became rare (made up).

What are common mistakes in a video player frontend?

The common mistakes are trusting one speed sample, flushing the buffer on a quality change, keeping library defaults on metered data, ignoring quota errors and shipping mismatched timestamps.

  • Jumping to the top level after one fast segment. The next slow segment then causes a stall.
  • Using an immediate switch such as hls.js currentLevel for a manual quality change. It flushes the buffer and creates the stall the viewer wanted to avoid.
  • Leaving maxMaxBufferLength at 600 seconds for viewers on prepaid data.
  • Ignoring QuotaExceededError in a hand-written MSE player. Appends fail and the buffer stops filling.
  • Renditions whose timestamps do not match. The RFC says matching content in variant streams must have matching timestamps, so clients can synchronize the media.

How do you explain a video streaming frontend in a system design interview?

Explain it as a pipeline from playlists to pixels, then spend most of your time on how the player chooses quality and manages the buffer. Tie every choice to a viewer on a weak network.

  1. Ask about viewers and networks

    Ask which devices and networks matter. Viewers on slow phones lead to very different choices than viewers on smart TVs.

  2. Describe the files

    Explain renditions, short aligned segments and the two playlist levels. Draw the bitrate ladder with close rungs at the bottom.

  3. Draw the player parts

    Show the fetcher, ABR controller, buffer, MSE and video element, with arrows for data and decisions.

  4. Explain the quality choice

    Combine a smoothed speed estimate with the buffer level. Go down fast, go up slowly, one rung at a time.

  5. Cover startup and limits

    Start low, cap to screen size, set both a floor and a ceiling for the buffer, and handle the browser's quota error.

  6. Name the metrics

    Startup time, rebuffer ratio, average bitrate and switch count, measured in the lab and from real sessions.

Questions people ask

What is the difference between HLS and adaptive bitrate streaming?
HLS is a format, and adaptive bitrate is a behaviour. HLS defines how playlists and segments are written so a player can find every quality. Adaptive bitrate is the player's logic that picks which quality to fetch next. RFC 8216 leaves that switching algorithm to the client, so the rules live in the player.
Does hls.js buffer 30 seconds ahead by default?
Not usually. In hls.js 1.7.3, 30 seconds is the maxBufferLength floor. The real goal is the time needed to fill 60 MB at the level's peak bitrate, capped by maxMaxBufferLength at 600 seconds. At a 360 kbps peak that is the full 600 seconds. Set maxMaxBufferLength to 30 if you want a true 30 second ceiling.
What segment length should I use for HLS?
RFC 8216 calls 10 seconds a typical target duration, but nothing requires that length; the team in this guide uses 4 seconds. Shorter segments let the player react faster and start sooner on slow networks. They also mean more requests and more playlist entries, so test a few lengths on your own content and network traces.
Do I need Media Source Extensions to play HLS in a browser?
Not always. Safari plays an HLS playlist directly in a video element. Elsewhere you need MSE, usually through a library such as hls.js that reads the playlists and appends segments for you. The hls.js README recommends trying hls.js first and native playback second, because native support varies between browsers.
Is buffer-based ABR better than throughput-based ABR?
Neither wins everywhere, which is why players mix them. A 2014 study at Netflix found that choosing from the buffer cut the rebuffer rate by 10 to 20 percent against its then-default algorithm. The same study found speed estimates matter most at startup, when the buffer is empty. Combining both signals is a sound default.
How do I simulate a slow network for testing video playback?
Use the network throttling presets in browser developer tools for quick checks. For serious testing, record real speed traces on phones and replay them through a throttling proxy. Real traces include sudden dips and recoveries that a fixed slow speed never shows, and those dips are what cause stalls.

Sources and further reading

Practise this with Lala

Practise