ZeroBuffer
PricingDocsNetworkBlogsContact
Sign inStart free
ZeroBuffer
PricingDocsNetworkBlogsContact
Sign inStart free
  1. Home
  2. /
  3. Blogs
C4 — HLS Playback & PlayersTechnical tutorial with production-readiness checklist

HLS.js Demo: Build a Working Video Player

Build an HLS.js demo with native Safari fallback, error recovery, CORS checks, React cleanup, and a production-ready video playback test plan for launch.

By Sahil AsopaAugust 6, 202611 min read2,448 wordsView as Markdown
Developer workstation showing adaptive HLS streams flowing into a browser player
Developer workstation showing adaptive HLS streams flowing into a browser player

An HLS stream that plays in VLC can still fail the moment you put it in a browser. The quickest way to separate a bad manifest from a bad player integration is to build a small HLS.js demo that exposes each playback step instead of hiding everything behind a framework.

This guide gives you a complete vanilla JavaScript player, native HLS fallback for Safari, visible status messages, fatal-error recovery, a React version with correct cleanup, and a test plan you can use before shipping.

What does an HLS.js demo prove?

An HLS.js demo proves that a browser can fetch your .m3u8 manifest, parse its renditions, load compatible media segments, and append them to an HTML <video> element through Media Source Extensions. A successful demo also confirms that cross-origin rules, TLS, codecs, and the basic playback lifecycle work in the browser you are testing.

It does not prove that every viewer will get a good experience. Production readiness also depends on cross-browser coverage, startup time, buffering, adaptive bitrate behavior, signed URL lifetime, captions, analytics, and CDN performance.

HLS.js is a playback engine rather than a complete player interface. It handles HLS loading and adaptive playback while the browser's video element supplies standard controls; the official HLS.js project documents compatibility with browsers that expose Media Source Extensions and a supported video/MP4 input.

Before you build the HLS.js demo

Start with a direct HLS manifest URL. A watch-page URL, an MP4, or an API endpoint that returns JSON is not interchangeable with a .m3u8 playlist.

Check these five inputs first:

  1. A testable manifest: Use a stream you own or a public sample intended for development. Apple's HLS example streams cover multiple codecs, captions, and advanced features, although not every codec is supported on every device.
  2. HTTPS from end to end: An HTTPS page should not load an HTTP manifest, segment, key, or caption file. Mixed-content blocking can look like a player failure.
  3. Consistent CORS headers: The manifest, child playlists, segments, encryption keys, and subtitle files may live at different URLs. Every cross-origin response used by JavaScript needs an appropriate policy.
  4. Browser-decodable codecs: HLS.js can parse a playlist but cannot make the device decode an unsupported codec combination.
  5. No accidental authorization expiry: A signed master URL is not enough if the segment or key URLs expire before the viewer requests them.

For the first pass, use the public Mux test manifest already referenced by HLS.js examples. Once the demo works, replace it with your own URL and compare the network traces.

HLS.js demo: copy-paste HTML player

Save the following as index.html, serve it from a local web server, and open it in a modern browser. It uses the major-version HLS.js CDN URL shown in the project's official API usage guide, so the page does not require a build step.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>HLS.js demo</title>
    <style>
      body {
        max-width: 960px;
        margin: 40px auto;
        padding: 0 20px;
        color: #e5eefc;
        background: #07111f;
        font: 16px/1.5 system-ui, sans-serif;
      }
      video { width: 100%; aspect-ratio: 16 / 9; background: #000; }
      #status { min-height: 1.5em; color: #8fd3ff; }
      #status[data-error="true"] { color: #ff9b9b; }
    </style>
  </head>
  <body>
    <h1>HLS.js demo player</h1>
    <video id="video" controls playsinline preload="metadata"></video>
    <p id="status" role="status" aria-live="polite">Starting player…</p>

    <script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
    <script>
      const source =
        'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
      const video = document.querySelector('#video');
      const status = document.querySelector('#status');

      function setStatus(message, isError = false) {
        status.textContent = message;
        status.dataset.error = String(isError);
      }

      function requestPlayback() {
        video.play().catch(() => {
          setStatus('Ready. Press play to start; autoplay was blocked.');
        });
      }

      if (Hls.isSupported()) {
        const hls = new Hls({
          enableWorker: true,
          capLevelToPlayerSize: true,
        });

        hls.loadSource(source);
        hls.attachMedia(video);

        hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
          setStatus(`Manifest parsed: ${data.levels.length} quality levels.`);
          requestPlayback();
        });

        hls.on(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
          const level = hls.levels[data.level];
          if (level?.height) {
            setStatus(`Playing ${level.height}p in automatic quality mode.`);
          }
        });

        hls.on(Hls.Events.ERROR, (_event, data) => {
          console.error('HLS.js error', data);
          if (!data.fatal) return;

          if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
            setStatus('Fatal network error. Retrying…', true);
            hls.startLoad();
          } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
            setStatus('Fatal media error. Attempting recovery…', true);
            hls.recoverMediaError();
          } else {
            setStatus(`Unrecoverable error: ${data.details}`, true);
            hls.destroy();
          }
        });

        window.addEventListener('pagehide', () => hls.destroy(), { once: true });
      } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
        video.src = source;
        video.addEventListener('loadedmetadata', () => {
          setStatus('Loaded with the browser\'s native HLS engine.');
          requestPlayback();
        });
      } else {
        setStatus('This browser supports neither HLS.js nor native HLS.', true);
      }
    </script>
  </body>
</html>

Run it through a server instead of double-clicking the file:

npx serve .

Open the localhost URL printed by the command. If the manifest parses, the status line shows the number of quality levels and then reports automatic rendition switches.

How the HLS.js demo works

The demo has two playback paths because support is a runtime question, not a user-agent question.

1. Detect the engine before loading the stream

Hls.isSupported() checks whether HLS.js can use the browser's media pipeline. When it returns false, video.canPlayType('application/vnd.apple.mpegurl') checks for native HLS. This fallback matters on Apple platforms, where direct HLS playback may be the appropriate path even when the JavaScript engine is unavailable or unnecessary.

If both checks fail, changing the manifest URL will not solve the problem. You need a supported browser, another delivery format, or a player that supports the target environment.

2. Load the manifest and attach the video element

loadSource() gives HLS.js the master or media playlist URL. attachMedia() connects the HLS.js instance to the <video> element and creates the media-source plumbing used to append playable buffers.

The MANIFEST_PARSED event is the first useful success checkpoint. It means the top-level playlist was fetched and understood, but playback can still fail later on a variant playlist, segment, encryption key, or decoder.

3. Let adaptive bitrate start in automatic mode

The example does not force currentLevel. HLS.js can therefore choose a rendition based on its bandwidth estimate and buffer state. capLevelToPlayerSize: true prevents automatic selection from needlessly exceeding the displayed player size, while the LEVEL_SWITCHED listener makes the selected resolution visible.

Keep manual quality selection out of the first demo. It adds UI state before you have proved that normal automatic playback works.

4. Separate expected errors from terminal failures

HLS playback involves repeated network requests, so a recoverable retry should not page an engineer. The HLS.js API guide distinguishes fatal errors and documents startLoad() for network recovery and recoverMediaError() for media recovery.

The demo logs the complete event object for diagnosis but changes the UI only for fatal failures. A production player should also cap recovery attempts; endlessly retrying an expired URL or invalid manifest wastes bandwidth and hides the real fault.

HLS playback pipeline with manifest checks, adaptive streams, and a blocked request path

Turn the HLS.js demo into a production-ready player

A green demo is the beginning of integration, not the end. Harden the boundaries around the player before adding custom controls.

Pin dependencies and control upgrades

The sample pins the HLS.js major version with @1, following the official usage pattern. For a production build, install hls.js from npm, commit your lockfile, and upgrade deliberately. Test startup, seeking, captions, encrypted content, and error recovery against the same manifest set on every change.

Avoid @latest in a deployed script tag. A release should not change your playback engine without passing through your test pipeline.

Treat autoplay as optional

video.play() returns a promise and may reject when playback with sound lacks user interaction. The demo catches that rejection and leaves native controls available instead of presenting it as a stream error.

If autoplay is a product requirement, design for muted playback or a clear play action. MDN's autoplay guide recommends handling policies that may allow, block, or allow only inaudible media.

Fix CORS at the delivery layer

HLS.js fetches resources under browser security rules. MDN describes CORS as an HTTP-header mechanism that lets a server authorize other origins; JavaScript cannot grant itself that permission.

Apply the policy to the master manifest, every variant manifest, .ts or .m4s segments, keys, captions, and error responses. When cookies or authorization headers are involved, use an explicit allowed origin and credential-aware configuration rather than a wildcard.

Make the delivery path observable

Record player version, stream identifier, startup time, fatal error type and detail, selected level, rebuffer events, and the CDN request ID when available. Do not log signed URLs or viewer credentials.

Once playback logic is stable, segment delivery becomes a separate systems problem. ZeroBuffer's OTT CDN is designed to cache HLS manifests and media segments at the edge, with origin shielding and published pay-as-you-go pricing; it is a practical delivery layer to test when global egress or origin load—not JavaScript—is the constraint.

Destroy the instance when its owner disappears

Single-page apps can leave listeners, workers, requests, and media buffers alive after navigation if the HLS.js instance is never destroyed. Give each mounted player one instance, destroy it during cleanup, and create a fresh one when the source changes.

Use HLS.js in React without leaking an instance

The playback logic is the same in React, but ownership must follow the component lifecycle:

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

export function HlsPlayer({ src, poster }) {
  const videoRef = useRef(null);
  const [error, setError] = useState('');

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

    setError('');

    if (Hls.isSupported()) {
      const hls = new Hls({ capLevelToPlayerSize: true });
      hls.loadSource(src);
      hls.attachMedia(video);

      hls.on(Hls.Events.ERROR, (_event, data) => {
        if (!data.fatal) return;
        setError(`Playback failed: ${data.details}`);
      });

      return () => hls.destroy();
    }

    if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = src;
      return () => {
        video.removeAttribute('src');
        video.load();
      };
    }

    setError('HLS playback is not supported in this browser.');
  }, [src]);

  return (
    <div>
      <video ref={videoRef} controls playsInline poster={poster} />
      {error && <p role="alert">{error}</p>}
    </div>
  );
}

The cleanup function is the important part. It runs before the effect is recreated for a new src and when the component unmounts, preventing the previous stream from continuing in the background.

If you want a ready-made UI rather than a custom video element, compare a wrapper such as the Video.js HlsJsVideo component. A wrapper can reduce control and accessibility work, while direct HLS.js integration gives you tighter access to events and configuration.

Troubleshoot a broken HLS.js demo

Start with the earliest failed request or event. Later errors are often consequences.

Symptom Likely cause First check
No manifest request appears Script did not load or initialization did not run Console errors, Content Security Policy, and the HLS.js script response
Manifest request is blocked CORS, mixed content, DNS, or TLS Browser console and response headers on the exact .m3u8 URL
Master loads but a variant fails Broken relative URL, expired signature, or inconsistent CORS First failing child playlist in the Network panel
Segments return 200 but playback fails Unsupported codec, malformed media, or timestamp discontinuity data.details, codec strings, and the same stream in the official demo
video.play() rejects Autoplay policy Keep controls visible; retry after a user gesture
Playback works in Safari but HLS.js events never fire Native HLS fallback is active Confirm which support branch ran
Live playback drifts or stalls Stale playlists, cache policy, encoder cadence, or insufficient throughput Playlist age, target duration, buffer, and segment timing
Old stream keeps downloading in React Instance cleanup is missing Verify hls.destroy() runs on source change and unmount

Use the project's official HLS.js demo as a control. If your stream fails there and in your page at the same request, investigate packaging or delivery. If it succeeds there but fails in your page, compare HLS.js versions, configuration, request credentials, CSP, and application lifecycle.

Test the player before release

Use a small matrix instead of relying on the browser open on your laptop:

  • Run one known-good VOD stream, one live stream, and one intentionally invalid URL.
  • Test Chrome, Firefox, Edge, macOS Safari, and a real iPhone or iPad relevant to your audience.
  • Confirm the native fallback path as well as the HLS.js path.
  • Throttle the network and watch automatic quality changes and buffer recovery.
  • Seek near the start, middle, and end of VOD; for live, seek within the DVR window and return to the live edge.
  • Verify captions, alternate audio, poster, fullscreen, picture-in-picture, and keyboard operation when your product exposes them.
  • Expire a signed URL on purpose and confirm the viewer gets an actionable message rather than an infinite spinner.
  • Navigate away or change sources and confirm network requests from the old instance stop.

The result should be repeatable: a known stream passes, a broken stream fails visibly, and the logs identify which layer failed without exposing secrets.

Frequently asked questions

Can HLS.js play an M3U8 file?

Yes. HLS.js loads .m3u8 HLS playlists in browsers that provide a compatible Media Source Extensions pipeline. The media codecs listed by the playlist must still be decodable on the device.

Does Safari need HLS.js?

Not always. Safari supports native HLS, so a common implementation assigns the manifest directly to video.src when the HLS.js support check is false. Keep both feature-detected paths because support varies by platform and browser version.

Why does my HLS stream work in VLC but not in the browser?

Browsers enforce CORS, mixed-content, autoplay, and codec rules that a desktop player may handle differently. Inspect the first failed manifest, segment, key, or subtitle request in browser developer tools before changing HLS.js settings.

How do I know the HLS.js demo is working?

Confirm that MANIFEST_PARSED fires, video frames render, audio plays when expected, and quality levels switch under throttling. Also test a deliberate failure so you know the status and recovery paths work.

Can I use HLS.js with React or Next.js?

Yes. Create the HLS.js instance in a client-side effect after the video element exists, and destroy it in the effect cleanup whenever the source changes or the component unmounts.

Build the smallest player that proves the whole path

Choose direct HLS.js when you need a custom interface, detailed playback events, or precise control over adaptive streaming. Choose a mature player wrapper when accessible controls, plugins, and faster UI delivery matter more than owning the engine integration.

Either way, keep this demo as a diagnostic fixture. Replace the sample with one production manifest, run the browser matrix, and measure startup, rebuffering, and fatal errors before you tune advanced options or move real traffic.

Topics covered

hls js demojs hls

On this page

  • What does an HLS.js demo prove?
  • Before you build the HLS.js demo
  • HLS.js demo: copy-paste HTML player
  • How the HLS.js demo works
  • Turn the HLS.js demo into a production-ready player
  • Use HLS.js in React without leaking an instance
  • Troubleshoot a broken HLS.js demo
  • Test the player before release
  • Frequently asked questions
  • Build the smallest player that proves the whole path

Global delivery without the egress tax

119 edge locations, 25ms average latency, free video encoding — at a flat $0.0049/GB for every region.

Start for freePricing

Keep reading

C4 — HLS Playback & Players

HLS Player Guide: How to Choose the Right Tool

August 6, 202612 min
CDN Providers & Competitive Landscape

CDN for Video: The True Cost, the Best Options, and How to Cut Your Egress Bill

July 31, 202618 min
Video Streaming CDN & Delivery (C5)

Akamai CDN Video: Speed, Cost, and Best Fit

August 6, 202611 min
All articles

ZeroBuffer™

High-performance CDN, storage, streaming and optimization — built for global scale.

Product

  • Home
  • About Us
  • Features
  • Network
  • Use Cases
  • Pricing
  • Contact Us

Solutions

  • CDN for OTT
  • CDN for Gaming

Company

  • Blogs
  • Contact
  • Careers

Legal

  • Privacy Policy
  • Terms of Service
  • Acceptable Use Policy
  • Refund Policy
  • Report Abuse
© 2026 Apexnova Private Limited. All rights reserved.