iTranslated by AI
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
- The explanation of hls.js is very easy to understand, so you'll get it if you read it.
- API.md | video-dev / hls.js | GitHub
- @types/hls.js | yarnpkg
- replace | yarnpkg
- For TypeScript and yarn setup, please refer to my past article:
- 2020/11 edition: Setting up a development environment for TypeScript+UmbrellaJS+PostCSS with yarn
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.
- Examples | HTTP Live Streaming | Apple Developer
- Basic Stream | HTTP Live Streaming Examples | Apple Developer
- HTTP Live Streaming | Wikipedia
- Playing by specifying a blob URL in the src of the video tag | NER
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.
- video-dev / hls.js | GitHub
- "To build our distro bundle and serve our development environment we use Webpack."
- https://github.com/video-dev/hls.js
- Provide an ES Modules build for modern browsers/tools #2910 | video-dev / hls.js | GitHub
- Since this issue is not closed, an ESModule version might be released in the future...
- (Though you/I could also contribute it...)
- https://github.com/video-dev/hls.js/issues/2910
- How to import Hls.js from another file #2911 | video-dev / hls.js | GitHub
- "There is no ES6 export in the JavaScript dist."
- https://github.com/video-dev/hls.js/issues/2911
Oh no.
So, after trying various things, I decided on the following approach. It's quite brute-force.
- Introduce only the type definition file for hls.js.
- Write code by importing it in TypeScript.
- Transpile it.
- 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
- @types/hls.js | yarnpkg
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>
- API.md | video-dev / hls.js | GitHub
- hls.js demo
- I borrowed the HLS file used in the sample from here.
- It is distributed with
"Access-Control-Allow-Origin: *".
- It is distributed with
- https://hls-js.netlify.app/demo/
- I borrowed the HLS file used in the sample from here.
- javascript - How to handle "Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first." on Desktop with Chrome 66? - Stack Overflow
- Since Google Chrome is designed so that videos cannot be played without user action,
-
video.muted = trueis required for autoplay. - It might be kinder to provide a play button.
- I wonder how YouTube and Niconico manage to autoplay.
- https://stackoverflow.com/questions/49930680/how-to-handle-uncaught-in-promise-domexception-play-failed-because-the-use
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:
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
- Streaming playback with AWS+hls.js (Part 2) | Qiita
- Easy-to-understand explanation of video streaming implemented with hls.js | YukiPress
- Let's become a contributor to hls.js | ART OF LIFE
- Hls.js is super convenient. | Digital Farm Co., Ltd.
- Recommended HLS web player: Trying out hls.js | Country of Orange
Discussion