Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Vueとhls.jsを使って、AndroidとiOSでいい感じにm3u8を再生する

フロントエンドエンジニアのこん(@k0n_karin)です! 今回は昨年末に書いたウェブストーリー関連のお話となります。

tech.yappli.io

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の処理を以下を参考にしながら書いていきます!

github.com

・・・そして、できたものがこちらになります!(お料理番組のノリ

github.com

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をロードしていきます。

再生可能状態になるとHTMLVideoElementcanplayイベントで教えてくれます。再生できるようになったら実行したいことが将来あるかもしれないので、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ファイルを再生できるようになりました!🥳

Android
iOS

おわりに

平成を生きた自分は、令和になっても動画といえばmp4だったので、m3u8とtsを再生できようやく令和を生きる事ができました。また、Vue3でcomposition APIをもりもり書いて、composableとして切り出す楽しさも満喫しております。

一方で、OSごとに開発言語を切り替える必要なく実装できるWebViewとはいえ、OSごとに挙動が異なることは日常茶飯事です。今回はOSをまたぐマルチプラットフォームの辛さと、うまく処理をかけたときの達成感の両方を味わうことができました。WebViewはカスタムされていることも多々あり、勝手が違うこともありますが、新鮮で楽しかったです。🙌

HLSが大好きな方やcomposableを作るのが大好きな方、ヤプリに興味を持ったという方はぜひ一度カジュアルにお話ししてみませんか?

open.talentio.com