HLS形式の再生中動画から画像キャプチャを取得する

# 目的

HLS形式の再生中動画から画像キャプチャを取得したいのでその方法を検証する

# 使うもの

Docker
VSCode(Remote Development拡張機能使用)
Vue(Vite)

# 開発環境の準備

ローカルには何も入れないで仮想環境上で開発していきます。

## VSCode で開発用の空フォルダを開く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/d9dfc3e8-3718-ffcc-db52-a03ab1747fe2.png)

フォルダ名は hls_capture としました

## 仮想環境用の Dockerfile を用意

```dockerfile:Dockerfile
FROM node:lts
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/0b28d89a-2c2f-8a41-9272-cb1431031e19.png)

## VSCode で Reopen in Container, From 'Dockerfile' を実行して仮想環境を起動

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/39c835c9-e62f-1063-c059-dbeb352656d2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/7bd482e8-5537-b2b4-dea6-f5a360128ce3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/6f9a3ff4-a3b5-f833-5a53-3c3c32b3acb2.png)

## ターミナルを起動し node バージョン確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/d38d04f8-47a8-cbf3-532d-c554c12bdea9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/d67ec0bc-b3fd-f370-65de-bb7527fa3c98.png)

## vue インストール

今回は vite　を使いました。

```sh:terminal
yarn create vite . --template vue
```
途中の上書き確認は y, Package name には　package.json と入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/5432fa2e-b739-a67a-dd57-f09c2bac9ebc.png)

フォルダを作りたくないので . を指定しましたが、そうすると Dockerfile が消えてしまうので、再作成しておきます。

```dockerfile:Dockerfile
FROM node:lts
```

一度ローカルに戻って再度仮想環境を開き直します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/cd08ef57-63fc-3879-afab-84f7b3050b11.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/39c835c9-e62f-1063-c059-dbeb352656d2.png)

yarn で必要なモジュールをインストールします。

```sh:terminal
yarn
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/cd722478-0ae4-6a3e-75b3-2652fe42edeb.png)


## 開発サーバ起動

NPM SCRIPTS の dev の起動ボタンを押します。
※　NPM SCRIPTS が表示されていない場合は package.json ファイルを選択すると表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/3e1d5a4f-e96b-4e06-69d7-e2287f318a00.png)

## ブラウザで確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/5a8cb523-c4f4-4674-6383-15527c2742b2.png)

起動すると Open in Browser ボタンが表示されるので押してブラウザを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/e8a8c6c3-885e-5d8b-87e1-8a52a245daaf.png)

## 拡張機能追加

仮想環境で使う vue の拡張機能(Volar)を devcontainer.json に追加します。

```json:.devcontainer/devcontainer.json変更前
"extensions": []
```

```json:.devcontainer/devcontainer.json変更後
"extensions": ["johnsoncodehk.volar"]
```

Rebuild Container を実行して拡張機能をインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/796cfbf0-0837-ed9a-43d1-cb78e3dac783.png)

これで開発環境の準備が完了です。

# HLSの再生に対応したコンポーネント作成

## トップページの編集

ビデオ表示枠と画像表示枠、画像ダウンロードボタンを配置します。

```vue:src/App.vue
<script setup>
</script>

<template>
  <h1>HLSライブ動画のキャプチャ</h1>
  <article>
    <section>
      <video src="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" autoplay playsinline muted />
      <button type="button">キャプチャ</button>
    </section>
    <section>
      <img />
      <button type="button">ダウンロード</button>
    </section>
  </article>
</template>

<style>
h1 {
  text-align: center;
}
article {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}
section {
  display: flex;
  gap: 10px;
  flex-direction: column;
  justify-content: center;
}
video, img {
  width: 684px;
  height: 360px;
  border: 1px solid silver;
}
</style>
```

サンプル動画は HLS.js のものを使わせていただいています。
ブラウザで確認すると以下のようになります。
2022/2現在、mac だと Safari では再生されますが、Chrome, Edge, Firefox では再生されません。

||
|-|
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/93f62094-bfac-f77d-d9e6-bb901ed473ed.png)

## HlsVideo.vue ファイルを作成

```vue:src/components/HlsVideo.vue
<template>
  <video/>
</template>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/6c6034a3-ed37-9982-da4e-e8259f84374b.png)

## HlsVideo コンポーネントを読み込みます

```vue:src/App.vue
<script setup>
import HlsVideo from './components/HlsVideo.vue';
</script>

<template>
  <h1>HLSライブ動画のキャプチャ</h1>
  <article>
    <section>
      <HlsVideo src="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" autoplay playsinline muted />
      <button type="button">キャプチャ</button>
    </section>
    <section>
      <img />
      <button type="button">ダウンロード</button>
    </section>
  </article>
</template>

(以下省略)
```

import 行と videoタグを HlsVideo　に置き換えました。
ブラウザでの表示は変わらないはずです。

## HLS.js を追加

対応していないブラウザでも再生できるように HLS.js というライブラリを組み込みます。

まずターミナルから hls.js を追加します。

```sh:terminal
yarn add hls.js
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/47e93f64-eb5a-fb4b-071a-a39d39064dee.png)

## HlsVideo.vue 編集

HLS.js を使ってビデオを読み込む処理を追加します。

```vue:src/components/HlsVideo.vue
<template>
  <video ref="video" />
</template>

<script setup>
import { onMounted, onBeforeUnmount, ref, watch } from "vue"
import Hls from "hls.js"

const props = defineProps({
  src: String
})

const video = ref()
const hls = new Hls()
const updateSrc = (src) => {
  hls.detachMedia()
  if (!src) {
    return
  }
  if (video.value.canPlayType && video.value.canPlayType('application/vnd.apple.mpegurl')) {
    video.value.src = src
  } else if (Hls.isSupported()) {
    hls.loadSource(src)
    hls.attachMedia(video.value)
  }
}

onMounted(() => updateSrc(props.src))

watch(() => props.src, src => updateSrc(src))

onBeforeUnmount(() => updateSrc())
</script>
```

video.value.canPlayType でブラウザが対応しているか調べて対応していればそのまま video の src　をセットします。
HLS.js に対応しているブラウザの場合は hls を介して再生されるよう video を設定しています。

これで Chrome, Edge, FireFox でも動画が再生されるようになりました。

# 画像キャプチャ機能追加

HlsVideo コンポーネントに capture メソッドを追加します。

```vue:src/components/HlsVideo.vue
<template>
  <video ref="video" />
</template>

<script setup>
import { onMounted, onBeforeUnmount, ref, watch } from "vue"
import Hls from "hls.js"

const props = defineProps({
  src: String
})

const video = ref()
const hls = new Hls()
const updateSrc = (src) => {
  hls.detachMedia()
  if (!src) {
    return
  }
  if (video.value.canPlayType && video.value.canPlayType('application/vnd.apple.mpegurl')) {
    video.value.src = src
  } else if (Hls.isSupported()) {
    hls.loadSource(src)
    hls.attachMedia(video.value)
  }
}
const capture = (then, format = 'image/png') => {
  const width = video.value.videoWidth
  const height = video.value.videoHeight
  const canvas = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  canvas.getContext('2d').drawImage(video.value, 0, 0, width, height)
  then(canvas.toDataURL(format))
}
defineExpose({ capture })

onMounted(() => updateSrc(props.src))

watch(() => props.src, src => updateSrc(src))

onBeforeUnmount(() => updateSrc())
</script>
```

キャプチャボタン、ダウンロードボタンの処理を追加します。

```vue:src/App.vue
<script setup>
import HlsVideo from './components/HlsVideo.vue'
import { ref } from 'vue'

const video = ref()
const image = ref()
const download = () => {
  const a = document.createElement('a')
  a.download = 'capture.png'
  a.href = image.value.src
  a.click()
}
</script>

<template>
  <h1>HLSライブ動画のキャプチャ</h1>
  <article>
    <section>
      <HlsVideo ref="video" src="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" autoplay playsinline muted crossorigin="anonymous" />
      <button type="button" @click="video.capture(url => image.src = url)">キャプチャ</button>
    </section>
    <section>
      <img ref="image" />
      <button type="button" @click="download">ダウンロード</button>
    </section>
  </article>
</template>

<style>
h1 {
  text-align: center;
}
article {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}
section {
  display: flex;
  gap: 10px;
  flex-direction: column;
  justify-content: center;
}
video, img {
  width: 684px;
  height: 360px;
  border: 1px solid silver;
}
</style>
```

これでキャプチャとダウンロードができるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/0c0cbfcb-db89-ac55-eb71-17626dcfc589.png)

ただし、動作確認すると、 Chrome, Edge, FireFox は問題ないですが、 Safari だけキャプチャ画像が透明になってしまうようです。
https://github.com/video-dev/hls.js/issues/1806
ISSUEを見るとブラウザの動作なのでライブラリ側ではどうしようもないようです。

仕方がないのでキャプチャ画像が透明な場合はクリップボードの画像を貼り付ける処理をいれて、
Safari の場合は Shift + Command + 4 で手動でクリップボードでキャプチャしたものを貼り付けられるようにしておきます。

```js:src/components/HlsVideo.vue
//(省略)
const capture = (then, format = 'image/png') => {
  const width = video.value.videoWidth
  const height = video.value.videoHeight
  const canvas = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  const context = canvas.getContext('2d')
  context.drawImage(video.value, 0, 0, width, height)
  if (context.getImageData(0, 0, 1, 1).data.every(i => i == 0)) {
    navigator.clipboard.read().then(items => {
      const getImage = items.map(i => {
        const type = i.types.find(j => j.startsWith('image'))
        return type && (() => i.getType(type))
      })[0]
      if (getImage) {
        getImage().then(image => then(URL.createObjectURL(image)))
      }
    })
  } else {
    then(canvas.toDataURL(format))
  }
}
//(省略)
```

これで一応目的は達成できました。
safari が HLS ビデオの画像キャプチャに対応する日が来るのでしょうか。

# 今回のソース

https://github.com/nakazawaken1/hls_capture