← Blog.backToBlog

Build Your Own M3U8 Player with JavaScript

8 min read

Project Setup

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
  <input id="urlInput" placeholder="Paste M3U8 URL" />
  <button id="loadBtn">Load</button>
  <select id="qualitySelect"></select>
  <video id="video" controls></video>
  <div id="stats"></div>
</body>
</html>

Core Player Implementation

const video = document.getElementById("video");
const urlInput = document.getElementById("urlInput");
const loadBtn = document.getElementById("loadBtn");
let hls = null;

function loadStream(url) {
  if (hls) hls.destroy();
  if (Hls.isSupported()) {
    hls = new Hls();
    hls.loadSource(url);
    hls.attachMedia(video);
    hls.on(Hls.Events.MANIFEST_PARSED, updateQualityList);
  } else if (video.canPlayType("application/vnd.apple.mpegurl")) {
    video.src = url;
  }
}

loadBtn.addEventListener("click", () => {
  const url = urlInput.value.trim();
  if (url) loadStream(url);
});

Quality Selection UI

function updateQualityList() {
  const select = document.getElementById("qualitySelect");
  select.innerHTML = "";
  const auto = new Option("Auto", "-1");
  select.appendChild(auto);
  hls.levels.forEach((level, index) => {
    select.appendChild(
      new Option(
        level.height + "p (" + (level.bitrate / 1e6).toFixed(1) + " Mbps)",
        String(index)
      )
    );
  });
  select.onchange = () => {
    hls.currentLevel = Number(select.value);
  };
}

Simple Stats Panel

setInterval(() => {
  if (!hls || hls.currentLevel < 0) return;
  const level = hls.levels[hls.currentLevel];
  const buffered =
    video.buffered.length > 0
      ? (video.buffered.end(video.buffered.length - 1) - video.currentTime).toFixed(1)
      : "0.0";
  document.getElementById("stats").textContent =
    level.width + "x" + level.height + " | " +
    Math.round(level.bitrate / 1000) + " kbps | buffer " + buffered + "s";
}, 1000);

Error Handling Basics

Listen for Hls.Events.ERROR. Non-fatal errors can be ignored or retried; fatal network/media errors may need startLoad(), recoverMediaError(), or a full restart. Deep dive: hls.js not working. Primer: hls.js tutorial.

Ship Faster Without Building

If you only need a production-ready UI, embed or use our free online M3U8 player instead of maintaining your own controls.

Architecture Overview

A minimal JS M3U8 player is: + MSE + an HLS parser/loader (commonly hls.js) + UI for play/pause/level switching. Safari can use native HLS via the video element alone. Your job is lifecycle (attach/detach), error recovery, and not leaking MediaSource objects on SPA navigations.

// conceptual
import Hls from 'hls.js';
const video = document.querySelector('video');
if (Hls.isSupported()) {
  const hls = new Hls();
  hls.loadSource(url);
  hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
  video.src = url;
}

Production Concerns

TopicNotes
CORSOrigin must allow your site
LL-HLSNeeds newer player configs
DRMEME + license servers; not free hacks
AnalyticsTrack fatal vs nonfatal errors
MemoryDestroy hls instances on unmount

Test with the public reference UX patterns: clear errors, optional stats, quality selector.

Learning Path

Read hls.js tutorial, HTML5 player internals, and encryption. Ship a demo on a static host, then add retries and level locking. Do not scrape DRM catalogs as a “feature.”

Bottom Line

Building your own M3U8 player in JavaScript is approachable with hls.js + MSE, but production quality is error handling, lifecycle hygiene, and honest CORS/DRM limits — not a 10-line snippet alone.

Frequently Asked Questions

How hard is a custom HLS player?
Basic playback is straightforward with hls.js; polish takes longer.

Do I need React/Vue?
No — plain JS is enough.

How do I support Safari?
Detect native HLS and set video.src.

Can I add offline download?
Use a downloader pipeline separately from the player.