Chrome will not play an .m3u8 URL pasted into the address bar, because desktop Chrome has no native HLS decoder while Safari on macOS and iOS does. You have three practical options: use a hosted player that runs hls.js for you, embed hls.js in your own page through Media Source Extensions, or remux the playlist into a local MP4 with ffmpeg. The right choice depends on whether you are debugging a stream, shipping a web page, or just need an offline file.
How it works
An .m3u8 file is a UTF-8 text playlist, not a container of video bytes. A master playlist lists variant renditions with #EXT-X-STREAM-INF, and each media playlist lists short segments with #EXTINF and #EXT-X-TARGETDURATION. The player downloads the playlist, fetches segments (MPEG-TS or fragmented MP4), and appends them in order to a MediaSource buffer attached to a <video> element.
Browsers expose this through Media Source Extensions (MSE). Chrome supports MSE with video/mp4; codecs="avc1.42E01E,mp4a.40.2" but does not demux MPEG-TS by itself. hls.js performs that demuxing in JavaScript and transmuxes TS into fragmented MP4 before appending. You can confirm native support in any browser console:
const v = document.createElement('video');
console.log(v.canPlayType('application/vnd.apple.mpegurl')); // Safari: "maybe", Chrome: ""
console.log(v.canPlayType('application/x-mpegURL')); // usually "" in Chrome
console.log(window.MediaSource !== undefined); // true in Chrome
If canPlayType returns an empty string and MediaSource exists, hls.js is the correct path. Chrome on Android behaves differently: some builds report native HLS support, so always feature-detect instead of sniffing the user agent.
Step-by-step: three working methods
Method 1 - hosted player (fastest for debugging). Open the online HLS player, paste the playlist URL, and press play. The page loads hls.js from a CDN, attaches error handlers, and shows the network errors that a bare <video> tag hides. Use the stream inspector to see the variant list and segment timings without writing any code. This is the quickest way to answer whether the stream is broken or your page is broken.
Method 2 - hls.js in your own page. Save the following as player.html and serve it over HTTP (a file:// page will fail on module and CORS rules):
<!doctype html>
<html lang="en">
<body>
<video id="video" controls width="720"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js"></script>
<script>
const url = 'https://example.com/master.m3u8';
const video = document.getElementById('video');
if (Hls.isSupported()) {
const hls = new Hls({ enableWorker: true, lowLatencyMode: false });
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
console.error(data.type, data.details);
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();
else hls.destroy();
}
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
}
</script>
</body>
</html>
The server hosting the playlist must permit cross-origin reads. A minimal nginx block:
location ~* \.(m3u8|ts|m4s)$ {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Headers "Range, Origin" always;
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; video/iso.segment m4s; }
}
Method 3 - remux to a local MP4. When you own the content and only need a file, stream copy avoids re-encoding:
ffmpeg -allowed_extensions ALL -i master.m3u8 -c copy -movflags +faststart output.mp4
If the segments are MPEG-TS with AAC, older ffmpeg builds need -bsf:a aac_adtstoasc. If the source codec is not supported by the MP4 container, you must transcode instead of copying.
Common mistakes and parameters
| Method | Needs CORS | Needs code | Works offline | Best for |
|---|---|---|---|---|
| Hosted player | Yes, on the stream server | No | No | Fast debugging and QA |
| hls.js in your page | Yes, on the stream server | Yes | No | Production web players |
| ffmpeg remux | No | Command line | Yes | Archiving your own VOD assets |
| Setting | Where | Why it matters |
|---|---|---|
application/vnd.apple.mpegurl | nginx / CDN | Some players reject a playlist served as text/plain |
Access-Control-Allow-Origin | nginx / CDN | Without it, Chrome blocks the request that fetches the playlist |
enableWorker: true | hls.js config | Moves transmuxing off the main thread |
lowLatencyMode | hls.js config | Only useful when the playlist publishes LL-HLS parts |
-movflags +faststart | ffmpeg | Moves the MP4 index to the front for progressive playback |
Adding crossorigin="anonymous" to the <video> tag does not fix a missing CORS header; the header must come from the server that hosts the stream. A master playlist that points to absolute URLs on a second host also needs CORS on that host, not just on the playlist host. Finally, #EXT-X-KEY with AES-128 means the key URI is fetched by the player too, so that endpoint needs the same treatment.
Troubleshooting
- The console reports that a fetch has been blocked by CORS policy: add
Access-Control-Allow-Originto the playlist, every variant playlist, and every segment response, then retest withcurl -I. MANIFEST_LOAD_ERRORor a 404 on the playlist: verify the URL returns HTTP 200 with the right MIME type; a redirect to a login page is a common cause.FRAG_LOAD_ERROR/fragLoadError: the playlist loaded but a segment failed; check#EXTINFpaths, relative URL resolution, and a CDN that caches 404 responses.- The video stays black with no error: call
video.play()only afterMANIFEST_PARSED, and remember autoplay with sound is blocked until the user interacts. - A repeating
MEDIA_ERROR: runhls.recoverMediaError()once, then check that all variants share the same codec and GOP alignment. - A local
file://page fails even though the code is correct: serve the directory over localhost HTTP, since MSE and XHR behave differently under the file scheme.
Related reading
- Open the online player and stream inspector to check a playlist in seconds.
- Browse all HLS troubleshooting guides for CORS, buffering, and playlist errors.
- Inspect segment timing before you embed anything with the stream analyzer.
- How to Fix hls.js CORS Errors - diagnose cross-origin and playlist request failures.
- ffmpeg Command to Convert m3u8 to MP4 - remux a playlist into MP4 with ffmpeg.
- How to Test an HLS Stream Online - run a full playlist, segment and header check.