フロントエンドエンジニアのこん(@k0n_karin)です! 今回は昨年末に書いたウェブストーリー関連のお話となります。
HLSでm3u8を再生する
前回、動画はmp4を表示していましたが、モバイル端末のWebViewで再生することを考えて、HTTP Live Streaming(HLS)を使った動画再生に対応します。HLSで動画再生する際、セグメントに分割されたtsファイルとプレイリストとなるm3u8を扱う必要があります。これらはAndroid WebViewやChromeではMSEを使って再生できます。ここでは、簡単に扱えるhls.jsを使います。
WKWebViewやiOS Safariではネイティブで再生できますが、逆にMSEが使えません。😢
そのため、両OSに対応するべくhls.jsの処理とネイティブの処理をcomposable化します。🤝
早速、hls.jsの処理を以下を参考にしながら書いていきます!
・・・そして、できたものがこちらになります!(お料理番組のノリ
HLS用のcomposableを実装
まずはHLSを再生するためのいい感じのcomposableです。
import Hls from 'hls.js import { onMounted } from 'vue' import type { Ref } from 'vue' export function useHls( videoRef: Ref<HTMLVideoElement | null>, src: Ref<string>, onCanPlay = () => {} ) { const hls = new Hls() // m3u8の読み込みをする。MSEが使えるブラウザではHlsを使う const loadHlsMedia = (video: HTMLVideoElement, src: string) => { if (Hls.isSupported()) { hls.loadSource(src) hls.attachMedia(video) } else { video.src = src // 古いiOSではload()しないと正しく動画が初期化されず、再生できない時がある video.load() } } onMounted(() => { if (videoRef.value === null) { return } // oncanplayイベントのコールバックを設定 videoRef.value.oncanplay = onCanPlay // Hlsの読み込みを開始 loadHlsMedia(videoRef.value, src.value) }) }
そこまで多くないですね!では細かく見ていきます。
hls.jsを使ったm3u8ファイルの読み込みは、すべてJavaScriptで記述します。videoタグにsrcをバインドする必要はありません。公式のサンプルでもそのようになっているのがわかるかと思います。
一方、iOSで再生する場合は、いつもどおりsrcにバインドする必要があります。
上記をloadHlsMedia
関数で吸収します。
const loadHlsMedia = (video: HTMLVideoElement, src: string) => { if (Hls.isSupported()) { hls.loadSource(src) hls.attachMedia(video) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = src // 古いiOSではload()しないと正しく動画が初期化されず、再生できない時がある video.load() } }
Hls.isSupported()
はMSEが使えないブラウザではfalse
となります。今回MSEが使えるブラウザでは全てhls.jsで再生していきます。
そして、MSEが使えず、ネイティブでm3u8を再生できるブラウザはvideo.canPlayType('application/vnd.apple.mpegurl')
がtrue
となるため、iOSではこの分岐に入ります。それ以外のブラウザはちょっと知りません。(いんたーねっとえx
また、iOS用の処理では明示的にvideoをload()
しています。古いiOS Safariなどでは明示的にload()
しないと、動画再生時に怪しい挙動をすることがあります。(他のブラウザはよしなにやってくれるらしいです。🥲
そして、VueのonMounted
フックで実際にHLSをロードしていきます。
再生可能状態になるとHTMLVideoElement
はcanplay
イベントで教えてくれます。再生できるようになったら実行したいことが将来あるかもしれないので、useHls
にコールバック関数を渡せるようにしています。
さらなる拡張性として、第3引数をoptions
のようなオブジェクトにして、色々渡せるようにすると更に便利になりそうですね。🤔
onMounted(() => { if (videoRef.value === null) { return } // oncanplayイベントのコールバックを設定 videoRef.value.oncanplay = onCanPlay // Hlsの読み込みを開始 loadHlsMedia(videoRef.value, src.value) })
composableを利用するVueコンポーネントを実装する
次にcomposableを利用するVueコンポーネントです。useHls
を使うだけのシンプルな作りですね。スッキリ。😊
<script setup lang="ts"> import { ref } from 'vue' import { useHls } from './composables/useHls' const videoRef = ref<HTMLVideoElement | null>(null) const src = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8') const play = () => { if (videoRef.value === null) { return } videoRef.value.play() } useHls(videoRef, src, play) </script> <template> <main> <video ref="videoRef" muted playsinline></video> </main> </template> <style> video { width: 100%; } </style>
早速、m3u8のロードが完了したらそのまま動画再生したい気分だったので、useHls
にコールバックとしてplay
関数を渡しています。とても親切なcomposableですね。☺️
また、上記の通りユーザー操作ではなくJavaScriptから動画を再生する場合、videoタグにmuted
属性を指定する必要があります。これをしないとブラウザに怒られます。(怒られました。
Uncaught (in promise) domexception: play() failed because the user didn't interact with the document first.
これで晴れてそれぞれのデバイスでm3u8ファイルを再生できるようになりました!🥳
おわりに
平成を生きた自分は、令和になっても動画といえばmp4だったので、m3u8とtsを再生できようやく令和を生きる事ができました。また、Vue3でcomposition APIをもりもり書いて、composableとして切り出す楽しさも満喫しております。
一方で、OSごとに開発言語を切り替える必要なく実装できるWebViewとはいえ、OSごとに挙動が異なることは日常茶飯事です。今回はOSをまたぐマルチプラットフォームの辛さと、うまく処理をかけたときの達成感の両方を味わうことができました。WebViewはカスタムされていることも多々あり、勝手が違うこともありますが、新鮮で楽しかったです。🙌
HLSが大好きな方やcomposableを作るのが大好きな方、ヤプリに興味を持ったという方はぜひ一度カジュアルにお話ししてみませんか?