2022/01版:超強引にTypeScript+hls.jsでChromeでもvideoタグでHLS再生できるページ作る
2022/01/31 追記
この記事のPVがまだあるので2022/01/31現在の状況を調べてみましたが、まだChromeでは再生できないようです。
超強引なのでオススメしないやり方ですが、できました
TL;DR
- hls.js の解説がすごくわかりやすいので読めばわかります
- API.md | video-dev / hls.js | GitHub
- @types/hls.js | yarnpkg
- replace | yarnpkg
- TypeScript と yarn 周りは過去記事をご参照ください
- 2020/11版:TypeScript+UmbrellaJS+PostCSSの開発環境を yarn で構築する
Google Chrome では HTTP Live Streaming ファイルを video タグで再生できない
いろいろ調べてると2015〜2017年頃の記事で「Chromeでは再生できない」とあり
まぁ順次対応されてるんだろ…… とタカを括って2020/11時点,2022/01時点で見てみたら対応されてませんでした。
えぇ……(困惑)
テスト再生は Apple Developer の Examples | HTTP Live Streaming のサイトで
確認できるのですが、以下のような状態です
Google Chrome(ver86.0.4240.198 Mac版)
Google Chrome(ver97.0.4692.99 Mac版)
Mac safari(バージョン14.0 (15610.1.28.1.9, 15610))
えぇ……(困惑)
なので、対応が必要です。ま、まぁFireFoxも再生できないみたいだし多少はね?
Youtube は Blob URL を使ってサーバーからストリーミングを流しているようですし
ちゃんとストリーミング受信の処理を各々用意してねってことなんでしょうか。知らんけど。
しょうがないので hls.js を導入して再生できるようにしましょう。
- Examples | HTTP Live Streaming | Apple Developer
- Basic Stream | HTTP Live Streaming Examples | Apple Developer
- HTTP Live Streaming | Wikipedia
- video タグの src に blob URL を指定して再生する | NER
hls.js は ESModule 対応版が無い(2020/11時点)
ここが一番困りました。
hls.js の README.md にもISSUEにもあるんですが
npm から持ってきて自前で配信するなら webpack 使ってね! とのことみたいです。
- video-dev / hls.js | GitHub
- "To build our distro bundle and serve our development environment we use Webpack."
- 「バンドルを構築して開発環境にサービスを提供するには、Webpackを使用します。」
- https://github.com/video-dev/hls.js
- Provide an ES Modules build for modern browsers/tools #2910 | video-dev / hls.js | GitHub
- このISSUEがcloseになってないので将来的にはESModule対応版が出るかも……
- (じゃあキミ(私)がやったら良いじゃんってのはある……)
- 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
そんなぁ。
ということでいろいろ試しまして、以下の方針で行くことにしました。かなり強引です。
- hls.js の型定義ファイルだけを導入
- TypeScript で import して書く
- トランスパイルする
- トランスパイル後に import 文のあたりは置換で消す
正直、一度作ってしまえばあまり触るようなところでもないので
こんな方法で TypeScript で実装できるようにしなくても
webpack なしで hls.js を導入するなら
hls.js を使う部分だけ JavaScript で書いちゃった方が良いですね。
全くオススメできないですが、今回はやってみてできたので記事に書いちゃおうと思いまして……。
hls.js の型定義だけを導入する
hls.js は型定義ファイルが DefinitelyTyped で提供されています。
yarn add @types/hls.js --dev
- @types/hls.js | yarnpkg
hls.js で動画を再生する
まぁこんな感じです。
hls.js の解説がすごくわかりやすいので読めばわかります。
hls.js demo のサイトのソースコードを読むのも早いです。
./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();
});
}
再生側のhtmlはこんな感じにします。hls.js は CDN から持ってくるようにします。
source URL は非表示DOMから持ってくることにしました。
<!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
- サンプルに使うHLSファイルもここからお借りしました
-
"Access-Control-Allow-Origin: *"
で配信されてるので
-
- https://hls-js.netlify.app/demo/
- サンプルに使うHLSファイルもここからお借りしました
- 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
- Google Chrome はユーザーのアクションなしには動画を再生できない様になっているので
- 自動再生させるなら
video.muted = true
が必要 - 再生ボタンを出して上げる方が親切かな。
- Youtubeやニコニコ動画は自動再生するけどどうなってるんだろ
- https://stackoverflow.com/questions/49930680/how-to-handle-uncaught-in-promise-domexception-play-failed-because-the-use
トランスパイル後に replace でimport 文のあたりは置換で消す
こんなことをします。わぁお強引。
トランスパイル後に import
を抜いて、コードを改変するから 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",
},
で、こうします。
yarn ttsc
yarn replacehls
yarn replacemap
こんな JavaScript になります。
vidoElement.style.display = "block";
してるのは再生の準備が完了するまで
要素を非表示にしてるからです。上記HTML側で 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();
});
}
できた
できました。
今回のリポジトリはここの一部にしてあります。
なんで上記実行画面でミュートになってるの?
前述しましたが、
Google Chrome はユーザーのアクションなしには動画を再生できない様になっているので
自動再生させるなら video.muted = true
が必要です。
再生ボタンを出して上げる方が親切かなとは思いました。
Youtubeやニコニコ動画は自動再生するけどどうなってるんだろう……。
なんで webpack 使わないの?
私もそう思います。
なんで非表示要素に source url 埋め込んでるの?
Ruby on Rails の ERB でレスポンスするときに埋め込んで置ければ
JavaScript 側でURLを取得するのが楽だからです。
ページ表示してから fetch API で取得してきても良いんですけどね。
その他の参考サイト
- AWS+hls.jsでストリーミング再生(その2) | Qiita
- hls.jsで実装する動画ストリーミングの分かりやすい解説 | YukiPress
- hls.js のコントリビューターになろう | ART OF LIFE
- Hls.jsが超便利。 | 株式会社デジタルファーム
- おすすめhls web player hls.jsを使ってみる | オレンジの国
Discussion