HTTP Live Streaming (HLS) is the industry standard for delivering video across the internet. However, not all web browsers support HLS natively. While Safari handles .m3u8 files out of the box, Chrome, Firefox, and Edge require a JavaScript polyfill like hls.js to parse the manifest and feed the video segments into the browser's Media Source Extensions (MSE) API.
Integrating hls.js with React introduces unique challenges because React components mount, unmount, and re-render unpredictably, whereas hls.js relies on persistent instances and imperative DOM mutations. In this guide, you will learn how to build a robust, leak-free React video player component that seamlessly plays M3U8 streams.
Why hls.js and not native video tags?
If you simply pass an M3U8 URL into an HTML5 <video src="stream.m3u8"> element, it will fail on most desktop browsers. hls.js bridges this gap by intercepting the HLS manifest, downloading the `.ts` or `.m4s` segments, and feeding them to the video element.
Step 1: Installation
Start by installing the hls.js package in your React project:
npm install hls.js
# or
yarn add hls.js
Step 2: Building the HlsPlayer Component
To use hls.js in React, you need to manage two crucial pieces of state using hooks:
useRefto hold the reference to the actual<video>DOM node.useEffectto initialize, attach, and ultimately destroy theHlsinstance during the component lifecycle.
Here is the foundational code for a robust HLS player component:
import React, { useEffect, useRef } from 'react';
import Hls from 'hls.js';
const HlsPlayer = ({ src, autoPlay = true, controls = true }) => {
const videoRef = useRef(null);
useEffect(() => {
let hls;
const video = videoRef.current;
if (video) {
// 1. Check if the browser supports hls.js (Media Source Extensions)
if (Hls.isSupported()) {
hls = new Hls({
// You can pass configuration options here
maxBufferLength: 30,
maxMaxBufferLength: 600,
});
// Bind the HLS instance to the video element
hls.attachMedia(video);
// Listen for the MEDIA_ATTACHED event to load the source
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(src);
});
// Listen for the MANIFEST_PARSED event to optionally auto-play
hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (autoPlay) {
video.play().catch(e => console.warn("Autoplay prevented:", e));
}
});
}
// 2. Fallback for browsers with native HLS support (like Safari)
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src;
video.addEventListener('loadedmetadata', () => {
if (autoPlay) {
video.play().catch(e => console.warn("Autoplay prevented:", e));
}
});
}
}
// 3. Cleanup: destroy the hls instance on unmount to prevent memory leaks
return () => {
if (hls) {
hls.destroy();
}
};
}, [src, autoPlay]); // Re-run effect if the source URL changes
return (
<div className="hls-player-wrapper">
<video
ref={videoRef}
controls={controls}
style={{ width: '100%', height: 'auto' }}
/>
</div>
);
};
export default HlsPlayer;
Understanding the Cleanup Function
The return () => { hls.destroy(); } block is absolutely critical. If you omit this, every time the component re-renders or the user navigates away, a new HLS instance will spawn in the background, downloading segments and consuming massive amounts of RAM, eventually crashing the browser tab.
Step 3: Handling Errors Gracefully
In the real world, streams fail. A viewer might lose their internet connection, or an M3U8 token might expire, causing 403 Forbidden errors. You must attach error listeners to the hls.js instance to handle these gracefully rather than letting the player silently break.
// Add this inside the Hls.isSupported() block
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error("Fatal network error encountered, trying to recover...");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error("Fatal media error encountered, trying to recover...");
hls.recoverMediaError();
break;
default:
// Cannot recover
console.error("Unrecoverable error, destroying hls instance");
hls.destroy();
break;
}
}
});
By catchingNETWORK_ERRORandMEDIA_ERROR, your player can automatically heal itself during brief Wi-Fi dropouts or minor manifest corruption without requiring the user to refresh the page.
Step 4: Managing Dynamic Source Changes
What if you want to switch the video stream dynamically, like in a live TV application? Because our useEffect has [src] in its dependency array, React will automatically re-run the effect when the URL changes. The cleanup function will run, destroying the old HLS instance, and a fresh instance will attach to the video node with the new URL.
This declarative pattern ensures your application state and player state remain perfectly synchronized.
Advanced: Exposing Quality Selection
HLS uses Adaptive Bitrate Streaming (ABR), which means hls.js automatically adjusts the quality based on the user's bandwidth. However, many applications require a manual quality selector (e.g., forcing 1080p). You can read the available levels using hls.levels and set the manual level using hls.currentLevel.
hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
const availableQualities = data.levels.map(level => level.height);
console.log("Available resolutions:", availableQualities);
// To force the highest quality:
// hls.currentLevel = data.levels.length - 1;
});
To implement a UI for this, you would save data.levels into a React useState array, render a dropdown, and update hls.currentLevel when the user selects a specific height. Note that if you want to store the hls instance so it can be accessed by button clicks outside the useEffect, you should store it in a useRef(null) to avoid triggering unnecessary re-renders.
Summary
Implementing hls.js in a React application revolves entirely around understanding the React component lifecycle. By wrapping the imperative hls.js API inside a useEffect hook, managing the video DOM element via a useRef, and strictly enforcing the hls.destroy() cleanup phase, you can build production-ready, performant HLS video players that delight your users.