Video Player

hls.js: What It Is, How It Works, and How to Use It

16 min read
hls.js JavaScript code on a developer monitor
Reading Time: 11 minutes

If you build a web video player and point a <video> tag at an .m3u8 file in Chrome, Firefox, or Edge, nothing plays. Those browsers do not parse HLS playlists or TS segments on their own. Safari does — every other major browser needs help. That help is hls.js, a small JavaScript library that has become the default way to deliver HLS streams to the web outside the Apple ecosystem.

This guide walks through what hls.js is, how it talks to the browser’s video element, how to install and configure it, how to handle adaptive bitrate switching and errors, and how it compares to alternatives like Shaka Player, Video.js, and dash.js. You will see real code snippets you can paste into a project, plus the integration patterns developers use with React and other frameworks.

What Is hls.js?

hls.js is an open-source JavaScript library that plays HTTP Live Streaming (HLS) video in browsers that do not have native HLS support. It implements the client side of the HLS specification in pure JavaScript and feeds the decoded media to a standard HTML5 <video> element through the Media Source Extensions (MSE) API.

The project lives at video-dev/hls.js on GitHub under the Apache 2.0 license. It is used by Twitch, JW Player, Akamai, Vimeo, BridTV, Clappr, MediaElement.js, Kaltura, Video.js, Flowplayer, Fluid Player, and dozens of other production players and platforms.

Attribute Value
Type JavaScript HLS playback library
License Apache 2.0
Dependencies HTML5 <video> + Media Source Extensions
Containers supported MPEG-2 TS, fragmented MP4 (fMP4)
Video codecs H.264, H.265 (HEVC)
Audio codecs AAC, MP3, AC-3, E-AC-3
Encryption AES-128, SAMPLE-AES, FairPlay, Widevine, PlayReady
Captions CEA-608/708, WebVTT, IMSC1
Bundle size ~70 KB gzipped (light build smaller)

The library does not ship its own UI controls. It is the engine — you bring a <video> element (or a player framework that wraps one), and hls.js handles playlist parsing, segment downloads, transmuxing, and quality switching.

How Does hls.js Work?

hls.js sits between the HLS playlist on a server and the <video> element in the page. The flow is short but worth understanding because most production issues trace back to one of these steps.

  1. Fetch the master playlist. hls.js downloads the master .m3u8 file and parses out the variant streams (different bitrates, resolutions, codecs) and any alternate audio or subtitle renditions.
  2. Pick an initial quality. Based on configured bandwidth estimates, viewport size, and startLevel settings, it picks a starting variant and downloads that variant’s media playlist.
  3. Download segments. Each variant playlist lists media segments (typically .ts or .m4s files, a few seconds each). hls.js fetches them with the Fetch or XHR loader.
  4. Transmux when needed. If segments arrive as MPEG-2 TS, hls.js repackages them into ISO BMFF (fragmented MP4) on the fly because MSE only accepts fMP4. This transmuxing runs in a Web Worker when available so it does not block the main thread.
  5. Push to MSE. Transmuxed buffers are appended to a MediaSource SourceBuffer that is attached to the <video> element via a blob URL.
  6. Switch and recover. hls.js measures actual download throughput each segment and switches up or down between variants. If a segment fails, it retries, falls back to a lower variant, or emits an error event you can hook into.

The library exposes the full state of this pipeline through events — MANIFEST_PARSED, LEVEL_LOADED, FRAG_LOADING, FRAG_BUFFERED, ERROR, and roughly 50 more — which is what makes it possible to build custom analytics, quality selectors, and error recovery on top.

Browser Support for hls.js

hls.js runs anywhere the browser exposes the Media Source Extensions API with video/mp4 MIME type support. That covers most modern browsers but not every device.

Browser hls.js support Native HLS support
Chrome 47+ (desktop) Yes No
Firefox 51+ (desktop) Yes No
Edge (Chromium) Yes No
Safari 10+ (macOS) Yes Yes
Safari iOS 10+ Limited (no MSE on iOS before 17.1) Yes
Safari iOS 17.1+ Yes Yes
Safari iPadOS 13+ Yes Yes
Chrome / Firefox on Android 5+ Yes No
Samsung Internet Yes No

The interesting case is Safari. macOS Safari and modern iOS/iPadOS Safari support HLS natively through the plain <video> src attribute. Older iPhone Safari (pre-17.1) does not expose MSE, so hls.js cannot run there — but native HLS works fine on those devices, so you do not need it.

The standard pattern is feature detection: if hls.js is supported, use it; otherwise, fall back to native HLS if the <video> element reports it can play application/vnd.apple.mpegurl. The basic example below shows that pattern.

Key Features of hls.js

hls.js is more than a playlist parser. The feature set lines up with what production video apps need.

Adaptive Bitrate Streaming

hls.js implements adaptive bitrate streaming (ABR) with three modes: an ABR controller that picks levels automatically based on measured bandwidth, manual level switching where your UI sets hls.currentLevel, and capped auto switching where you constrain the max level (useful for forcing 720p ceilings on data-conscious users).

Live and VOD Support

The same engine handles VOD playlists and live playlists. For live streams, hls.js tracks the live edge, supports DVR seeking inside the configured window, and can auto-recover when the live window slides past the current playhead.

Low-Latency HLS (LL-HLS)

hls.js implements Apple’s Low-Latency HLS extension, including partial segment fetching, blocking playlist reload (_HLS_msn and _HLS_part query parameters), and preload hints. With a well-tuned origin, you can hit 2–4 seconds of glass-to-glass latency over standard HTTP CDNs. For a deeper look at the protocol, see our guide on low-latency streaming.

DRM and Encryption

hls.js supports AES-128 and SAMPLE-AES clear-key encryption out of the box, plus full DRM via the Encrypted Media Extensions (EME) API. That means FairPlay (Safari), Widevine (Chrome, Firefox, Android, smart TVs), and PlayReady (Edge, Xbox) all work with the same JavaScript code, as long as you wire up a license server.

Captions, Subtitles, and Alternate Audio

CEA-608/708 closed captions embedded in the video stream are decoded and rendered as TextTracks. WebVTT subtitle renditions from the playlist are loaded automatically. Multiple audio renditions (languages, descriptive audio) appear as audio tracks you can switch with hls.audioTrack.

Error Recovery

The library distinguishes between network errors (segment download failures, manifest 404s) and media errors (decode failures, buffer stalls). Each emits a typed ERROR event with details and a fatal flag. You can call hls.startLoad(), hls.recoverMediaError(), or hls.swapAudioCodec() to recover programmatically before tearing down the player.

Built-in Analytics Hooks

Every meaningful action emits an event: segment load duration, level switches, buffer underruns, dropped frames. Most QoE platforms (Mux Data, Datazoom, Bitmovin Analytics, NPAW) wire into these events with a few lines of glue code.

How to Use hls.js

Getting a basic hls.js player running takes about ten lines of HTML and JavaScript. The library is small, has no required runtime dependencies, and works with any build system.

Install via CDN

The fastest path is loading hls.js from a CDN. jsDelivr, unpkg, and cdnjs all serve the package.

<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>

Pin the major version (@1) so a future breaking change does not surprise your production page. For development, you can pin to a specific patch (@1.6.16).

Install via npm

For bundled apps, install from npm:

npm install hls.js

Then import it:

import Hls from 'hls.js';

There is also a hls.js/dist/hls.light.mjs build that strips out subtitles, alt-audio, and EME for a smaller bundle when you do not need those features.

Basic Playback Example

This is the canonical hls.js example, with native-HLS fallback for Safari:

<video id="video" controls></video>

<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
  const video = document.getElementById('video');
  const src = 'https://example.com/stream/master.m3u8';

  if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(src);
    hls.attachMedia(video);
    hls.on(Hls.Events.MANIFEST_PARSED, () => video.play());
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = src;
    video.addEventListener('loadedmetadata', () => video.play());
  }
</script>

That is the whole minimum. Hls.isSupported() returns true on any browser with MSE; the else if branch covers Safari, where you assign the .m3u8 URL directly to video.src.

Configuring hls.js

The constructor accepts a config object. A few of the most-used options:

const hls = new Hls({
  enableWorker: true,
  lowLatencyMode: true,
  maxBufferLength: 30,
  maxMaxBufferLength: 60,
  backBufferLength: 90,
  startLevel: -1,
  capLevelToPlayerSize: true,
  abrEwmaDefaultEstimate: 500000,
  fragLoadingTimeOut: 20000,
  manifestLoadingTimeOut: 10000,
});

capLevelToPlayerSize: true is the single most useful flag — it stops hls.js from downloading a 4K rendition into a 480-pixel-wide player on a phone. lowLatencyMode: true turns on LL-HLS behavior if the playlist advertises it.

Reacting to Quality Switches

To show the current quality in your UI or build a manual quality selector:

hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
  const level = hls.levels[data.level];
  console.log(`Switched to ${level.height}p @ ${level.bitrate} bps`);
});

// User picked 720p in your UI:
function setQuality(height) {
  const idx = hls.levels.findIndex(l => l.height === height);
  hls.currentLevel = idx; // -1 = auto
}

Handling Errors

Wire Hls.Events.ERROR and decide whether to recover or give up:

hls.on(Hls.Events.ERROR, (event, data) => {
  if (!data.fatal) return;

  switch (data.type) {
    case Hls.ErrorTypes.NETWORK_ERROR:
      hls.startLoad();
      break;
    case Hls.ErrorTypes.MEDIA_ERROR:
      hls.recoverMediaError();
      break;
    default:
      hls.destroy();
      // surface error to the user, maybe reload the page
  }
});

Non-fatal errors (a single segment 404 that hls.js retries on its own) are noisy in production logs. Filter on data.fatal === true before alerting anyone.

Using hls.js with React

hls.js has no React bindings of its own, but the integration is straightforward with useEffect and useRef. The key is destroying the Hls instance on unmount so segments stop downloading when the component leaves the page.

import { useEffect, useRef } from 'react';
import Hls from 'hls.js';

export function HlsPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    if (Hls.isSupported()) {
      const hls = new Hls({ lowLatencyMode: true });
      hls.loadSource(src);
      hls.attachMedia(video);
      return () => hls.destroy();
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = src;
    }
  }, [src]);

  return <video ref={videoRef} controls playsInline />;
}

For Vue, Svelte, or Angular, the same pattern applies: create the Hls instance when the component mounts, attach it to the video element, destroy it on unmount or when the source changes. For more on player patterns, see our walkthrough of building a React video player.

hls.js vs Alternatives

hls.js is the most popular but not the only choice. The right pick depends on which protocols you serve, whether you need DASH, and how much player framework you want bundled with the engine.

Library Protocol Bundle (gzipped) DASH support Live + LL License
hls.js HLS only ~70 KB No Yes (LL-HLS) Apache 2.0
Shaka Player HLS + DASH ~110 KB Yes Yes (LL-HLS, LL-DASH) Apache 2.0
dash.js DASH only ~140 KB Yes Yes (LL-DASH) BSD-3
Video.js (+ http-streaming) HLS + DASH ~250 KB with UI Yes Yes Apache 2.0
Native HTML5 video Depends on browser 0 KB No Limited N/A

hls.js is the right call if you only serve HLS and you do not need a UI bundled. It is small, focused, and has the largest production deployment footprint of any web HLS engine.

Shaka Player is the right call if you serve both HLS and DASH, or if you want a single library that handles offline downloads and a stricter EME implementation. Google maintains it and uses it on YouTube TV.

dash.js is right when you exclusively serve MPEG-DASH. It is the reference DASH client.

Video.js is right when you want a polished player UI out of the box. It uses an internal http-streaming engine that is similar in scope to hls.js but ships inside the larger Video.js bundle with controls and skinning.

Native HTML5 video is right for Safari on Apple platforms — there is no reason to ship hls.js to a user on iPhone Safari when the OS already has a hardware-accelerated HLS decoder. The pattern above (try hls.js, fall back to native) covers both.

For the protocol-level comparison rather than the player-level one, see our piece on WebRTC vs HLS and CMAF vs HLS.

Common hls.js Errors and How to Fix Them

A short list of the errors developers hit most often, and what causes them.

networkError / manifestLoadError — usually a CORS issue. The .m3u8 and every .ts or .m4s segment must be served with Access-Control-Allow-Origin set to your player’s origin (or *). Check the actual HTTP response headers, not your CDN config dashboard; some CDNs only emit CORS headers when the request includes an Origin header.

bufferStalledError — the playhead is sitting in a buffer hole. Often caused by segment download timeouts on flaky networks. Increase fragLoadingTimeOut, increase maxBufferLength, and verify segments are aligned to GOP boundaries on the encoder side. For mitigation patterns, our piece on how to avoid buffering covers the broader picture.

bufferAppendError / bufferAppendingError — the MSE SourceBuffer rejected the data. Almost always a codec mismatch: a variant switches mid-stream from H.264 Main profile to High profile, or audio sample rate changes between renditions. Re-encode so all variants share matching codec strings.

internalException from a Web Worker — hls.js loaded fine but cannot spawn its worker because of a strict Content Security Policy. Either allow worker-src 'self' blob: in your CSP or set enableWorker: false (slower but functional).

Native HLS used when you expected hls.jsHls.isSupported() returned false. On older iOS this is expected. Check your CSP and any browser extensions blocking MSE; on rare locked-down enterprise browsers, MSE is disabled.

404 on the .m3u8 but .ts segments load — your playlist URLs are relative and the base URL is wrong. Use absolute URLs in your master playlist or make sure your CDN preserves the path correctly.


The sections above answer the “what” and “how” of hls.js. The harder problem is what sits on the other side of the player — the encoder, packager, storage, and CDN that produce a healthy HLS stream in the first place. Below covers when hls.js is the right tool, the infrastructure it needs behind it, and the questions developers ask most often.

When to Use hls.js (and When Not To)

Reach for hls.js when:

  • You need HLS playback in Chrome, Firefox, Edge, or Android browsers.
  • You want a small, focused engine and you are building your own UI or already use a player framework.
  • You need fine-grained event hooks for analytics, manual quality switching, or custom error recovery.
  • You serve only HLS and have no plans to add DASH.

Reach for something else when:

  • You serve both HLS and DASH — Shaka Player handles both with one engine.
  • You need a complete player UI with controls — Video.js or JW Player ship the UI for you.
  • You are exclusively targeting Safari — native HLS through video.src is simpler and uses hardware decoding.
  • You need WebRTC-level latency (under 500 ms) — HLS, even LL-HLS, is not the right protocol.

The Infrastructure You Need Behind hls.js

hls.js only does one job: it plays a stream that already exists. To produce that stream, you need an ingest endpoint, a transcoder that emits multiple ABR renditions, a packager that writes the master playlist and segments, storage, and a CDN to deliver it all. Building that pipeline is where most teams burn months.

LiveAPI is a video infrastructure API that handles every step in front of hls.js. You push to the live streaming API over RTMP or SRT — or upload a VOD file through the video transcoding API — and LiveAPI returns an .m3u8 URL that is already packaged as ABR HLS, delivered through Akamai, Cloudflare, and Fastly. That URL drops straight into the hls.js example above with no other changes.

A few specifics that matter when you wire it to hls.js:

  • Multiple ABR renditions out of the box. Every stream is encoded to a ladder of qualities up to 4K. hls.js picks levels automatically based on bandwidth, and capLevelToPlayerSize keeps mobile players from pulling 4K.
  • Instant encoding. VOD uploads start playing in seconds, not minutes, so your test loop stays tight.
  • CMAF segments where supported. hls.js handles both MPEG-2 TS and fragmented MP4 — LiveAPI emits formats your player engine can consume.
  • Built-in HLS player. If you do not want to wire hls.js yourself, LiveAPI ships an embeddable HTML5 player that handles the engine choice for you across browsers. Use it as a baseline, then drop down to hls.js when you need custom UI.
  • Live to VOD. Live streams auto-record into VOD assets with the same HLS URL pattern, so the same hls.js page works for recordings.

For a broader view of what video infrastructure looks like in production, our piece on the live streaming SDK landscape and HLS streaming basics covers the wider context.

hls.js FAQ

Is hls.js free to use?

Yes. hls.js is released under the Apache 2.0 license, which permits commercial and modified use. The project is maintained by volunteers and sponsored organizations, with no runtime fees of any kind.

Does hls.js work in Safari?

Yes, but you usually do not need it there. Desktop Safari supports MSE and can run hls.js, while iOS and iPadOS Safari support HLS natively through <video src="…m3u8">. The standard pattern is to use hls.js when Hls.isSupported() is true, and fall back to native HLS otherwise.

How big is the hls.js bundle?

The full build is around 70 KB gzipped. The “light” build (hls.light.mjs), which drops subtitles, alternate audio, and EME, lands closer to 50 KB. For comparison, Shaka Player is around 110 KB and Video.js with its full UI is roughly 250 KB.

Can hls.js play DRM-protected streams?

Yes. hls.js wires into the browser’s Encrypted Media Extensions to support FairPlay, Widevine, and PlayReady. You configure a key system and a license server URL in the constructor, and the library handles the rest of the EME handshake.

What latency can hls.js achieve?

Standard HLS with hls.js runs at 8–30 seconds of glass-to-glass latency. With Low-Latency HLS enabled on both the origin and the player (lowLatencyMode: true), 2–4 seconds is achievable. For sub-second latency, WebRTC is the right protocol — hls.js cannot get there.

Does hls.js support m3u8 playlists with EXT-X-DISCONTINUITY?

Yes. Discontinuities are part of the HLS spec and hls.js handles them, including PTS resets across discontinuity tags. This matters for live streams that splice in ads or switch between distinct sources.

How do I switch to a specific quality manually?

Set hls.currentLevel to an index into the hls.levels array. Use -1 to return to auto-ABR. You can also use hls.nextLevel to schedule the switch for the next segment rather than the next playable chunk.

What is the difference between hls.js and Video.js?

hls.js is a playback engine — it parses the playlist and feeds data to a <video> element, with no UI. Video.js is a full player framework with controls, theming, plugins, and an internal HLS engine. Use hls.js if you are building custom UI; use Video.js if you want controls out of the box.

Can I use hls.js to download an HLS stream as MP4?

Not directly. hls.js plays streams in a browser; it does not provide a “save as MP4” pipeline. For converting HLS recordings, ffmpeg on the server is the right tool — or use a video API that exposes live-to-VOD assets as MP4 by default.

Wrapping Up

hls.js solved one specific problem — playing HLS where the browser refuses to — and it solved it well enough that it powers a large share of the web’s video traffic. The library is small, the API is small, and the integration in any modern web framework is a dozen lines of code. The complexity is not in the player; it is in everything that produces the stream.

If you want to skip the encoder, packager, storage, and CDN work and get an HLS URL you can paste into the hls.js example today, try LiveAPI free and have a 4K-capable, multi-CDN HLS stream running in minutes.

Join 200,000+ satisfied streamers

Still on the fence? Take a sneak peek and see what you can do with Castr.

No Castr Branding

No Castr Branding

We do not include our branding on your videos.

No Commitment

No Commitment

No contracts. Cancel or change your plans anytime.

24/7 Support

24/7 Support

Highly skilled in-house engineers ready to help.

  • Check Free 7-day trial
  • CheckCancel anytime
  • CheckNo credit card required

Related Articles