iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📹

Jan 2022: A Hacky Way to Enable HLS Playback in Chrome via <video> Tag with TypeScript and hls.js

に公開

Updated on 2022/01/31
I noticed this article still gets PV, so I checked the status as of 2022/01/31. It seems that Chrome still cannot play HLS natively.


This is a very brute-force method that I don't necessarily recommend, but I made it work.

TL;DR

Google Chrome cannot play HTTP Live Streaming files with the video tag

While researching, I found articles from around 2015–2017 stating "cannot play in Chrome."
I assumed it would have been supported by now... but checking as of 2020/11 and 2022/01, it still wasn't supported.
Wait, what... (confusion)

You can verify the test playback on Apple Developer's Examples | HTTP Live Streaming site, and the status is as follows:


Google Chrome (ver86.0.4240.198 Mac version)
Google Chrome (ver97.0.4692.99 Mac version)


Mac Safari (version 14.0 (15610.1.28.1.9, 15610))


Wait, what... (confusion)
Therefore, action is required. Well, I guess Firefox can't play it either, so it is what it is?
It seems YouTube uses Blob URLs to stream from the server, so maybe the intent is for everyone to prepare their own streaming reception logic? I don't know for sure.
Since there's no choice, let's implement hls.js to enable playback.

hls.js does not have an ESModule version (as of 2020/11)

This was the most troublesome part.
According to the README.md and issues for hls.js, if you want to pull it from npm and distribute it yourself, you are expected to use Webpack.

Oh no.
So, after trying various things, I decided on the following approach. It's quite brute-force.

  1. Introduce only the type definition file for hls.js.
  2. Write code by importing it in TypeScript.
  3. Transpile it.
  4. After transpilation, remove the import statement part using replacement.

Honestly, since it's not a part that you'll touch much once it's created, if you want to introduce hls.js without webpack, it might be better to just write the part using hls.js in JavaScript. I don't recommend this method at all, but since I managed to make it work, I decided to write about it in this article...

Installing only the type definitions for hls.js

Type definitions for hls.js are provided by DefinitelyTyped.

yarn add @types/hls.js --dev

Playing video with hls.js

Well, it looks like this.
The documentation for hls.js is very easy to follow, so you'll understand it by reading. Reading the source code of the hls.js demo site is also a quick way to learn.
./src/ts/video.ts

import Hls from "hls.js";

const videoSourceUrl = (document.getElementById("video_source") as HTMLSpanElement).innerText;
const vidoElement = document.getElementById("video") as HTMLMediaElement;
if (Hls.isSupported()) {
  const hls = new Hls();
  hls.loadSource(videoSourceUrl);
  hls.attachMedia(vidoElement);
  hls.on(Hls.Events.MEDIA_ATTACHED, function () {
    console.log("video and hls.js are now bound together !");
    vidoElement.style.display = "block";
    vidoElement.muted = true;
    vidoElement.play();

    hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
      console.log("manifest loaded, found " + data.levels.length + " quality level");
    });
  });
} if (vidoElement.canPlayType('application/vnd.apple.mpegurl')) {
  vidoElement.src = videoSourceUrl;
  vidoElement.addEventListener('canplay',function() {
    vidoElement.style.display = "block";
    vidoElement.play();
  });
}

The HTML for the playback side looks like this. We will load hls.js from a CDN.
I decided to retrieve the source URL from a hidden DOM element.

<!DOCTYPE html>
<html>
  <head>
    <title>video player test</title>
    <link href="./css/global.css" rel="stylesheet" type="text/css">
    <link href="./css/tailwindcss.css" rel="stylesheet" type="text/css">
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    <script type="module" src="./js/video.js"></script>
    <style>[hidden] { display: none !important; }</style>
  </head>
  <body>
    <h1>video player</h1>
    <video id="video" controls width="640" height="360" style="display: none;"></video>

    <div hidden>
      <span hidden id="video_source">https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"</span>
    </div>
  </body>
</html>

Remove import statements with replace after transpilation

This is what we'll do. Wow, that's brute-force.
It means removing import after transpilation and modifying the code, which also means deleting the sourcemap.

package.json

  "scripts": {
    "replacehls": "replace 'import Hls from \"hls.js\";' '' ./public/js/video.js",
    "replacemap": "replace '//# sourceMappingURL=video.js.map' '' ./public/js/video.js",
 },

Then, do this:

yarn ttsc
yarn replacehls
yarn replacemap

The resulting JavaScript will look like this.
The reason for vidoElement.style.display = "block"; is to keep the element hidden until it's ready for playback. In the HTML above, it's set to style="display: none;".
./public/js/video.js

const videoSourceUrl = document.getElementById("video_source").innerText;
const vidoElement = document.getElementById("video");
if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(videoSourceUrl);
    hls.attachMedia(vidoElement);
    hls.on(Hls.Events.MEDIA_ATTACHED, function () {
        console.log("video and hls.js are now bound together !");
        vidoElement.style.display = "block";
        vidoElement.muted = true;
        vidoElement.play();
        hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
            console.log("manifest loaded, found " + data.levels.length + " quality level");
        });
    });
}
if (vidoElement.canPlayType('application/vnd.apple.mpegurl')) {
    vidoElement.src = videoSourceUrl;
    vidoElement.addEventListener('canplay', function () {
        vidoElement.style.display = "block";
        vidoElement.play();
    });
}

It's Done

It worked.

The repository for this is part of the following:
https://github.com/JUNKI555/yarn_run_practice04

Why is it muted in the execution screen above?

As mentioned earlier,
Google Chrome is designed so that videos cannot be played without user action, so video.muted = true is required for autoplay.
I thought it might be kinder to provide a play button.
I wonder how YouTube and Niconico handle autoplay...

Why not use webpack?

I agree with you.

Why embed the source URL in a hidden element?

Because it's easier to retrieve the URL on the JavaScript side if it's already embedded when responding with Ruby on Rails' ERB.
Of course, fetching it via the Fetch API after the page loads would also be fine.

Other Reference Sites

Discussion