ラズパイに繋いだWEBカメラの映像を HLS 形式で配信してブラウザで再生する

ラズパイに繋いだWEBカメラの映像を HLS 形式で配信してブラウザで再生する検証を行ったので、その際の手順などを残しておきたいと思います。

## 構成

- Raspberry Pi 3B+
- WEBカメラ（USBカメラ）
  - ラズパイに接続します
  - 中古ショップで300円くらいで購入したものです
- Windows 10 Pro
  - クライアント機（配信された映像の再生用）

##### OS のディストリビューションとバージョン

```bash
$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description: Raspbian GNU/Linux 11 (bullseye)
Release: 11
Codename: bullseye
```

##### カーネル

```bash
$ uname -a
Linux raspberrypi 5.15.84-v7+ #1613 SMP Thu Jan 5 11:59:48 GMT 2023 armv7l GNU/Linux
```

##### カメラの対応フォーマットと解像度

解像度がとても低く、対応フォーマットも非常に少ないですね。

```bash
v4l2-ctl -d /dev/video0 --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
        Type: Video Capture

        [0]: 'MJPG' (Motion-JPEG, compressed)
                Size: Discrete 160x120
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 176x144
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 320x240
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 352x288
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 640x480
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
        [1]: 'YUYV' (YUYV 4:2:2)
                Size: Discrete 160x120
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 176x144
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 320x240
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 352x288
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
                Size: Discrete 640x480
                        Interval: Discrete 0.033s (30.000 fps)
                        Interval: Discrete 0.040s (25.000 fps)
                        Interval: Discrete 0.050s (20.000 fps)
                        Interval: Discrete 0.067s (15.000 fps)
                        Interval: Discrete 0.100s (10.000 fps)
                        Interval: Discrete 0.200s (5.000 fps)
```

## 検証

### 1. ffmpeg のインストール

ラズパイで以下のコマンドを実行して **ffmpeg** をインストールします。

```bash
$ sudo apt -y install ffmpeg
```

今回使用したバージョンは以下の通りです。

```bash
$ ffmpeg -version
ffmpeg version 4.3.5-0+deb11u1+rpt3 Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 10 (Raspbian 10.2.1-6+rpi1)
configuration: --prefix=/usr --extra-version=0+deb11u1+rpt3 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared --libdir=/usr/lib/arm-linux-gnueabihf --cpu=arm1176jzf-s --arch=arm
libavutil      56. 51.100 / 56. 51.100
libavcodec     58. 91.100 / 58. 91.100
libavformat    58. 45.100 / 58. 45.100
libavdevice    58. 10.100 / 58. 10.100
libavfilter     7. 85.100 /  7. 85.100
libavresample   4.  0.  0 /  4.  0.  0
libswscale      5.  7.100 /  5.  7.100
libswresample   3.  7.100 /  3.  7.100
libpostproc    55.  7.100 / 55.  7.100
```

### 2. ffmpeg 使って HLS ファイルを作成する

HLS 配信を行う際は、H.264 エンコード済みの動画を HLS 用の `.ts` ファイルに分割し、連番を付与しつつ `.m3u8` プレイリストを作成する必要があります。

今回は `stream` というディレクトリを作成し、そのディレクトリ内に `.m3u8` と `.ts` ファイルを作成するようにしました。

```bash
$ mkdir stream
$ ffmpeg -input_format yuyv422 -video_size 352x288 -framerate 30 -i /dev/video0 -f hls -c:v libx264 -hls_time 1 -hls_list_size 10 -hls_flags delete_segments stream/out.m3u8
```

以下、ffmpeg 実行時のオプション値の説明になります。
今回のように、事前にカメラの対応フォーマットや解像度を確認しておくと便利だと思います。

- `-input_format` ：入力フォーマット ※事前に確認した `YUYV` を指定
- `-video_size`：入力動画の解像度
- `-framerate`：入力動画のフレームレート
- `-i`：入力デバイス
- `-f`：出力フォーマット
- `-c:v`：出力ビデオのエンコード ※H.264 を指定
- `-hls_time`：`.ts` ファイルのセグメントの長さ ※今回は 1秒ごとに分割
- `-hls_list_size`：`.ts` ファイルをプレイリスト上に保持する個数
- `-hls_flags`：`delete_segments` を指定して、古い `.ts` ファイルを削除
- `stream/out.m3u8`：出力されるプレイリストのパス（名称）

コマンド実行すると、`stream` ディレクトリ内に以下のようなファイルが作成されます。
※余談ですが、ffmpeg の実行を停止する場合は、キーボードの「q」キーで停止できます。

```bash
$ ls -lh stream/
合計 2.3M
-rw-r--r-- 1 pi pi  352  1月 26 22:58 out.m3u8
-rw-r--r-- 1 pi pi 208K  1月 26 22:57 out10.ts
-rw-r--r-- 1 pi pi 203K  1月 26 22:57 out11.ts
-rw-r--r-- 1 pi pi 209K  1月 26 22:58 out12.ts
-rw-r--r-- 1 pi pi 194K  1月 26 22:58 out13.ts
-rw-r--r-- 1 pi pi 206K  1月 26 22:56 out3.ts
-rw-r--r-- 1 pi pi 211K  1月 26 22:56 out4.ts
-rw-r--r-- 1 pi pi 213K  1月 26 22:57 out5.ts
-rw-r--r-- 1 pi pi 211K  1月 26 22:57 out6.ts
-rw-r--r-- 1 pi pi 212K  1月 26 22:57 out7.ts
-rw-r--r-- 1 pi pi 211K  1月 26 22:57 out8.ts
-rw-r--r-- 1 pi pi 211K  1月 26 22:57 out9.ts
```

`out.m3u8` の中身は以下のようになっていました。

```bash
$ cat stream/out.m3u8 
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:4
#EXTINF:8.333333,
out4.ts
#EXTINF:8.333333,
out5.ts
#EXTINF:8.333333,
out6.ts
#EXTINF:8.333333,
out7.ts
#EXTINF:8.333333,
out8.ts
#EXTINF:8.333333,
out9.ts
#EXTINF:8.333333,
out10.ts
#EXTINF:8.333333,
out11.ts
#EXTINF:8.333333,
out12.ts
#EXTINF:7.833333,
out13.ts
#EXT-X-ENDLIST
```

### 3. カメラ映像確認用の HTML ページを作成する

クライアント機のブラウザからアクセスする用の **HTML ページ**を作成し、**WEB サーバ**を立てます。

ラズパイで、以下のHTMLファイルを作成し `stream` ディレクトリと同階層に配置します。
HTMLファイル内に登場する **hls.js** は、HLSクライアントの実装となる JavaScript ライブラリになります。
※ソースは、[こちらのサイト](https://hachu.net/raspberrypi/ubuntu-rpi-hls.html)から拝借して手を加えました。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <title>カメラ映像の配信</title>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
  </head>
  <body>
    <h1>HLSのサンプル</h1>
    <video id="sampleVideo" controls></video>
    <script>
      var video = document.getElementById('sampleVideo');
      var videoSrc = '/hoge/out.m3u8';
      if (Hls.isSupported()) {
          var hls = new Hls();
          hls.loadSource(videoSrc);
          hls.attachMedia(video);
      }
      else if (video.canPlayType('application/vnd.apple.mpegurl')) {
          video.src = videoSrc;
      }
    </script>
  </body>
</html>
```

ファイル・ディレクトリの位置関係を確認しておきます。

```bash
  $ tree .
.
├── index.html
└── stream
    ├── out.m3u8
    ├── out10.ts
    ├── out11.ts
    ├── out12.ts
    ├── out13.ts
    ├── out3.ts
    ├── out4.ts
    ├── out5.ts
    ├── out6.ts
    ├── out7.ts
    ├── out8.ts
    └── out9.ts
```

WEB サーバはどのような方法で立てても良いのですが、Python の `http.server` を使って WEB サーバを立てました。

```bash
$ python -m http.server 8000 --bind 0.0.0.0 --directory .
```

### 4. クライアント機のブラウザから HTML ページを表示する

ラズパイで WEB サーバを立てたら、クライアント機のブラウザからアクセスしてみます。
`http://<ラズパイのIPアドレス>:8000/`  でアクセスできます。

静止画なので非常に分かりづらいと思いますが、きちんとカメラ映像を再生できていることを確認できました。
※カメラで ffmpeg の公式サイトを撮影しているところです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759747/47cfa37f-24ab-be51-f926-fe481f4464c0.png)

30秒程度の遅延が発生していることも確認できたため改善の余地は沢山ありそうですが、
ひとまず、カメラ映像をブラウザで再生させるところまでは実現することができました。

## 注意点

SDカード内に大量の動画ファイルが作成されては消されていくため、やりすぎるとSDカードの寿命が縮まり壊れる可能性があります。
対策として「ラズパイ本体のメモリー領域である**RAM**に保存する」方法がありますので、興味のある方は試してみて下さい。

## ハマったところ

ffmpeg のオプションで `-c:v libx264` としている箇所ですが、当初、`-c:v copy` として「**入力フォーマット（今回だとYUYV422）でそのまま出力する**」ようにしていました。

この場合、きちんと `.ts` ファイルも作られて一見すると問題無さそうに見えたのですが、ブラウザからアクセスした際、いつまで経っても**再生が始まらない**という挙動になっていました。

そこで、HLS について調査を行ったところ「**H.264** にエンコードする必要がある」ことが分かり、最終的に`-c:v libx264` に辿り着きました。
ただし、エンコードを行っている分 `.ts` ファイルの作成にその分の時間がかかってしまっています。。

## 参考ページ

- [ubuntu 21.10(arm64) + Raspberry Pi 4環境における動画ストリーミング配信(HLS編)](https://hachu.net/raspberrypi/ubuntu-rpi-hls.html)
    - 今回の検証はこちらを参考にして行いました。とても参考になりました。