▶M3U8 SPACE

How to Play m3u8 in Chrome (3 Working Methods)

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

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

MethodNeeds CORSNeeds codeWorks offlineBest for
Hosted playerYes, on the stream serverNoNoFast debugging and QA
hls.js in your pageYes, on the stream serverYesNoProduction web players
ffmpeg remuxNoCommand lineYesArchiving your own VOD assets
SettingWhereWhy it matters
application/vnd.apple.mpegurlnginx / CDNSome players reject a playlist served as text/plain
Access-Control-Allow-Originnginx / CDNWithout it, Chrome blocks the request that fetches the playlist
enableWorker: truehls.js configMoves transmuxing off the main thread
lowLatencyModehls.js configOnly useful when the playlist publishes LL-HLS parts
-movflags +faststartffmpegMoves 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-Origin to the playlist, every variant playlist, and every segment response, then retest with curl -I.
  • MANIFEST_LOAD_ERROR or 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 #EXTINF paths, relative URL resolution, and a CDN that caches 404 responses.
  • The video stays black with no error: call video.play() only after MANIFEST_PARSED, and remember autoplay with sound is blocked until the user interacts.
  • A repeating MEDIA_ERROR: run hls.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