IPカメラの動画をウェブ・ページで公開する

自宅に設置した IP カメラの動画をウェブ・ページで公開する。

アホノミクスのおかげで、今や隠れもなき貧乏人の国となった「美しい日本」の私は、これを可能な限りの低予算で実現しようとする。このとき私が頼りとするのは、(1) 安価で高品質な中国製 IP カメラと、(2) 無料で高品質なフリー・ソフトウェア（オープン・ソース・ソフトウェア）と、(3) 耐用年数が過ぎた中古のノート・パソコンである。

前提条件として、(A) 光回線が引き込まれた家庭内 LAN と (B) インターネット上に設置済みのウェブ・サーバを想定している。(A) は必須である。(B) は無くても何とかなるだろうが、有るものとして話を進めてもこの記事の想定読者は文句を言うまい。

# 1 : IP カメラ

使用する IP カメラは SV3C ブランドの SV-801W-1080P-HX という機種で、主要諸元は以下の通りである。

- アルミ・ダイカスト筐体、IP66 防水・防塵
- 固定画角 200万画素 (1920 x 1080)
- 有線 / WiFi 両対応
- AC アダプタによる給電
- 赤外線暗視撮影
- 動体検知
- マイクロ SD カード対応（64 GB まで）
- RTSP 対応
- ONVIF 対応
- メール送信
- FTP アップロード

価格は Amazon.co.jp で 4,899 円（2020年10月現在）で最も廉価な部類に入るが、一見して堅牢な造りであり、この種の監視カメラとして期待される機能も一通り装備している。同じような製品は他にもあるが、サポート・サイトのマニュアル類が比較的充実している印象があるので SV3C のものを選択する。

**RTSP** と **ONVIF** に対応していることが重要だが、今時の IP カメラでは、対応していない方が珍しいだろう。

(追記)
二ヶ月間使用してみて、安定性に問題があることに気付いた。たとえば、CCD が飽和して画像が白飛びすることがある。また、たとえば、スナップ・ショットの JPEG が尻切れトンボになることがある。そして、これは他の原因による可能性もあるが、RTSP の通信が不安定になることが多い。
標榜する機能に対してチップの性能が低くて余裕が無いような感じだ。その点は値段相応なのかな、という感想である。

## 1-1 : IP カメラのセットアップ

屋外（家の軒端）に設置する前に、カメラのセットアップを行う。

以下の説明は SV3C SV-801W-1080P-HX を前提としているが、他社のカメラでもだいたい似たような手順になると思う。違っていたらごめん。

### 1-1-1 : マイクロ SD カード

最初に、一緒に買ったマイクロ SD カード（64GB 1,596 円）をカメラに装着する。

この製品は、レンズ・カバーを外して中身を引き出さないと、基板上のカード・スロットにアクセス出来ないので、作業の難易度が若干高い。しかし、一度挿入してしまえば二度と取り出すことのないカードであるから、これで良いと思う。

なお、カメラはカード無しでも動作する。動画配信のためにはカードは不要だし、防犯目的で使用するときでも NVR（Network Video Recorder）に接続する場合はカードは割愛できると思う。

しかし、カードを装着していれば、動画やスナップショットを自動的に記録保存できる。それに、設置後にカードを追加するのは作業が面倒だ。安いものだから、最初から装着しておく方が良いだろう。

### 1-1-2 : 家庭内 LAN に接続する

家庭内 LAN のルータ（またはハブなど）に LAN ケーブルで有線接続してカメラの電源を入れる。

これでルータの DHCP によって、カメラが動的 IP アドレスを取得することになる。

### 1-1-3 : CamHi

スマート・フォンに CamHi というアプリをインストールして起動する。このアプリによって家庭内 LAN に接続された IP カメラを検索して接続すると、動画を見たり、カメラの設定情報にアクセスしたりすることが出来るようになる。言うまでもないが、スマート・フォンは前もって WiFi で家庭内 LAN に接続しておく必要がある。

これ以降の設定作業も CamHi ですることが出来るが、ウェブ・ブラウザでカメラの設定画面にアクセスする方が楽なので、カメラの IP アドレスをメモするだけで CamHi は一旦終了する。（後で屋外設置工事の時に、画像の確認のためにもう一度使う。）

### 1-1-4 : ウェブ・ブラウザによる設定

ウェブ・ブラウザでカメラの IP アドレスにアクセスして、カメラの設定を行う。

ブラウザは **Internet Explorer** を使う。最初に動画再生用の ActiveX をインストールするように要求される。何じゃこれはと思うが、指示には従う。他のブラウザでも設定は可能だが、画像が表示されないので、いろいろと不便だ。あきらめて IE を使う。

設定可能な項目は多岐にわたる。よく分らない項目は初期設定のままで行くことにして、最低限必要と思われる下記の初期化・設定変更を行う。

- 管理者パスワードを変更する
- IP アドレスを静的アドレスとする (ここでは `192.168.0.101` とする)
- タイム・ゾーンを GMT +09:00 に設定する
- NTP サーバを ntp.nict.jp に設定する
- SD カードを初期化する

今回は設置場所および安定性を考慮して有線接続で行くことにするので、WiFi は OFF にする。また、目的が違うので、赤外線暗視、動体検知、メールや FTP によるアラームなど、防犯目的の機能は使用しない設定とする。

## 1-2 : IP カメラの屋外設置工事

動画の撮影場所である自宅の軒端に IP カメラを設置する。

スマート・フォンの CamHi で画像を確認しながら、設置位置や撮影方向を決めて固定する。広角レンズなので周辺部分がゆがむのは避けられないが、人間の目は水平方向の狂いに敏感なので、画像中心部分の水平・垂直に細心の注意を払ってカメラの角度を決める。

雨が当る場所なので LAN ケーブルと電源ケーブルの繋ぎ目は自己融着テープを使ってしっかりと防水処理をする。

## 1-3 : 静止画の配信

SV3C HX シリーズでは、ここまで来れば、静止画の配信が可能になる。

外部の FTP サーバに定期的にスナップショットをアップロードする機能があるので、これを利用して、ウェブ・サーバ側で適当に処理をすれば、ほぼリアルタイムの静止画をウェブ・ページで表示することが出来る。

![photo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12310/d8085419-0dfc-cf51-020c-18a2857a633c.png)

今回は動画配信が停止した場合に備えて静止画を配信するページを作成する。

### 1-3-1 : クライアント側 (HTML + javascript)

```
<img id="live-img" width="950" height="534" 
  src="/livecam/snapshot" 
  alt="ライブ・カメラのスナップ・ショット" />
```
画像のソースは `/livecam/snapshot` という URL である。`"snapshot.jpg"` のようにファイル名で指定するよりもサーバ側の仕事が楽になる。ファイル・キャッシュの悪影響を心配する必要もなくなる。

```
function updateImage() {
    $('#live-img').attr('src', '/livecam/snapshot');
}
var timer = setInterval(updateImage, 30000);
```

そして javascript のタイマーで 30 秒ごとにサーバに画像を要求する。

### 1-3-2 : サーバ側

サーバ側では、`/livecam/snapshot` という URL に対応するアクションから、クライアントに画像データを送信する。

あらかじめ指定されたディレクトリにカメラからのスナップショットが次々とアップロードされてきているので、その中で一番新しいファイルを選んで内容を読み込み、`'Content-Type: image/jpeg'` のヘッダを付けて送信すれば良い。

細かいことを言えば、画像のリサイズとか、不要なファイルの削除とか、面倒くさい処理がいくつか必要になるが、詳細は割愛する。

# 2 : 動画配信

## 2-1 : 概要図

![movie.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12310/7fb47516-5bce-c996-ac7d-580aa58704e8.png)

IP カメラからの動画を配信するためには、図で示すように、従来のウェブ・サーバに加えて、**動画変換サーバ**と**動画配信サーバ**が必要になる。

まず、**動画変換サーバ**が、カメラが出力する **RTSP** (Real Time Streaming Protocol) に従った動画データのストリームを **RTMP** (Real Time Messaging Protocol) に従ったデータに変換する。この動画変換サーバの実体は **ffmpeg** というアプリケーション・プログラムである。

そして、動画変換サーバから **RTMP** の出力を受けた**動画配信サーバ**が、**RTMP** のストリームや **HLS** (HTTP Live Streaming) プロトコルに変換したストリームをウェブ・クライアントに配信する。この動画配信サーバの実体は **nginx-rtmp-module** を装備した **NGINX** というウェブ・サーバ・プログラムである。

なお、今回は対応せず、図にも示していないが、この動画配信サーバは **MPEG-DASH** プロトコル (DASHはDynamic Adaptive Streaming over HTTP) に変換して配信することも可能である。

## 2-2 : 動画サーバ

中古のノート・パソコンを動画変換と動画配信を兼ねるサーバに仕立てて家庭内 LAN に設置する。NGINX から ffmpeg を起動するという形で連携をとるので、二つのサーバを一体化する方が都合が良い。

### 2-2-1 : Alma Linux 8

ノート・パソコンに Alma Linux 8 をインストールする。特に変ったことはしない。

何故 Alma Linux なのかと言うと、単に慣れ親しんだ CentOS から乗り換えただけであって、他に理由はない。どの linux ディストリビューションであっても良いはずだ。

この動画サーバの家庭内 LAN 上の IP アドレスは `192.168.0.99` とする。

### 2-2-2 : ffmpeg

ネット上には ffmpeg をソースからビルドする記事が数多く見受けられるが、そっちの方へ進むとたいてい遭難することになる。一昔前まではそうするしか仕方が無かったのだろうが、今はパッケージがあるので、余計な苦労をする必要は無い。

具体的には **RPM Fusion** レポジトリを使用して ffmpeg をインストールする。

まず、`EPEL` レポジトリが必要なのでインストールする。

```
# dnf install epel-release
# dnf config-manager --enable epel 
```

次に、レポジトリの `powertools` と `epel-playground` を有効にする。

```
# dnf config-manager --set-enabled powertools
# dnf config-manager --set-enabled epel-playground
```

`powertools` は、以前、`PowerTools` だったのだが、いつの間にか全部小文字になっていた。

そして `RPM Fusion` レポジトリをインストールする。

```
# rpm -ivh https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm
```

これで `ffmpeg` をインストールすることが出来る。

```
# dnf install ffmpeg
```

詳細は、[**"Install FFmpeg on CentOS 8 with RPMFusion repo"**] (https://www.rootlinks.net/2020/02/17/install-ffmpeg-on-centos-8-with-rpmfusion-repo/) という記事を参照されたい。タイトルは英語だが日本語の記事である。

### 2-2-3 : NGINX + nginx-rtmp-module

NGINX だけならパッケージを使えば良いのだが、nginx-rtmp-module を使うためにはソースを取得してビルドする必要がある。

#### 2-2-3-1 : パッケージ版の設定を記録する

しかし、順序としては、いったんパッケージ版をインストールする方が良い。

ビルドの設定やサービス登録など、パッケージ版の設定を記録して流用する方が NGINX の運用が楽になる。

OS 標準のパッケージはかなり古いので、かわりに NGINX 公式レポジトリのものを使う方が良いだろう。[**公式ページ (nginx:Linux Packages)**](http://nginx.org/en/linux_packages.html) の指示に従ってパッケージをインストールする。

インストール後に `nginx -V` でビルド設定を確認して記録すること、また、サービス設定ファイルである `/usr/lib/systemd/system/nginx.service` を別名で保存しておくこと。

そして、ビルドする前に、アンインストールする。

#### 2-2-3-2 : ビルドする

[**NGINX 公式サイト**](https://www.nginx.com) にある技術ブログ [**"Enabling Video Streaming for Remote Learning with NGINX and NGINX Plus"**](https://www.nginx.com/blog/video-streaming-for-remote-learning-with-nginx/) を参照して作業する。

最初に、ビルドに使うツールをインストールする。

```
$ sudo yum update
$ sudo yum groupinstall "Development Tools"
$ sudo yum install git
```

次に、依存パッケージをインストールする。

```
$ sudo yum install pcre-devel zlib-devel openssl-devel libxslt-devel gd-devel perl-ExtUtils-Embed
```

そして、適当なビルド用ディレクトリを作り、その中に入って `github` から `nginx-rtmp-module` と `nginx` のソースをクローンして取得する。

```
$ cd /path/to/build/dir
$ git clone https://github.com/arut/nginx-rtmp-module.git
$ git clone https://github.com/nginx/nginx.git
```

さらに `nginx` のディレクトリに入り、`nginx-rtmp-module` を設定に追加して、ビルドおよびインストールを実行する。

```
$ cd nginx
$ ./auto/configure \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--modules-path=/usr/lib64/nginx/modules \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=nginx \
--group=nginx \
--with-compat \
--with-file-aio \
--with-threads \
--with-http_addition_module \
--with-http_auth_request_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_mp4_module \
--with-http_random_index_module \
--with-http_realip_module \
--with-http_secure_link_module \
--with-http_slice_module \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_v2_module \
--with-mail \
--with-mail_ssl_module \
--with-stream \
--with-stream_realip_module \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-cc-opt='-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC' \
--with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' \
--add-module=../nginx-rtmp-module
$ make
$ sudo make install
```

上記、`configure` のパラメータは、パッケージ版 を `nginx -V` で起動して確認したものに `--add-module=../nginx-rtmp-module` を追加したものである。

これで NGINX が動くので、念のために確認する。

```
$ sudo nginx
```

動画サーバのホームページ (`http://192.168.0.99/`) に家庭内 LAN に接続されたパソコンからアクセスすると、NGINX のウェルカム・ページが表示される筈である。（ただしファイアウォールの設定が必要。後述する。）

`/usr/lib/systemd/system/nginx.service` を作成して（別名で保存していたファイルを復元して）、NGINX をサービスとして登録する。

```nginx.service
[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
# Nginx will fail to start if /run/nginx.pid already exists but has the wrong
# SELinux context. This might happen when running `nginx -t` from the cmdline.
# https://bugzilla.redhat.com/show_bug.cgi?id=1268621
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true

[Install]
WantedBy=multi-user.target
```

これで `systemctl (start|stop|restart|enable|disable) nginx` が出来るようになる。

まだ動画配信は出来ない（その設定をしていないから当然だ）が、**ffmpeg** と **NGINX + nginx-rtmp-module** さえ入れてしまえば、もう勝ったようなものである。（うーん、そうでもなかったな）

後は ffmpeg による動画変換の設定と NGINX による動画配信の設定だが、それに先だって動画サーバの運用のために必要となる環境整備の仕事を片付けておく。

### 2-2-4 : 動画サーバの環境整備

#### 2-2-4-1 : ファイアウォール

`http`, `https`, `RTSP`, そして `RTMP` のためにポートを開放する。

```
$ sudo firewall-cmd --permanent --add-service=http 
$ sudo firewall-cmd --permanent --add-service=https 
$ sudo firewall-cmd --permanent --add-service=rtsp
$ sudo firewall-cmd --permanent --add-port=1935/tcp
$ sudo firewall-cmd --reload
```

`1935/tcp` が `RTMP` の標準ポートである。`RTSP` は `554/tcp` と `554/udp` を標準ポートとするが、Alma Linux (CentOS) 8 では `--add-service=rtsp` を使って設定することができる。

#### 2-2-4-2 : ルータのポート・マッピング

インターネットから動画サーバにアクセス出来るように、家庭内 LAN のルータのポート・マッピング設定を変更して、WAN 側グローバル・アドレスへの着信が LAN 側の動画サーバのローカル・アドレス (`192.168.0.99`) に向うようにする。

`http(80)`, `https(443)`, `rtmp(1935)` だけでも構わないが、リモート管理のために `ssh(22)` もマップする。

#### 2-2-4-3 : DDNS (Dynamic DNS)

動画サーバを IP アドレスではなくドメイン名で指定できるように、DDNS (Dynamic DNS) サービスを利用する。

`nginx.conf` の `server_name` を DDNS のドメイン名 (`camsvr.on.ddns` であるとする) に変更して、NGINX を再起動する。

``` nginx.conf
http {
    server {
        server_name  camsvr.on.ddns;
```

このあたりで、ルータのポート・マッピングと DDNS が期待通りの動作をしているかどうか確認したいところだが、そのためには家庭内 LAN の外に出る必要がある。家庭内 LAN に接続された PC のウェブ・ブラウザで `http://camsvr.on.ddns/` にアクセスしても、動画サーバの NGINX ウェルカム・ページは表示されず、ルータのホームページ (`http://192.168.0.1/`) が表示される。要するに LAN 内からのアクセスに対してはポート・マッピングが機能しないのである。テストのためには WiFi を切ったスマート・フォンのブラウザを使うのが良いだろう。

DDNS の自動更新については [**"CentOS8 で DDNS の自動更新 (MyDNS.JP 専用)"**](https://qiita.com/softark/items/a07b7dcc295309de3c17) を参照。

#### 2-2-4-4 : Let's Encrypt

予定では、動画配信のウェブ・ページは、既存のウェブ・サーバが提供するページに、新設の動画サーバからのコンテンツが混じる形になる。ウェブ・サーバは既に `SSL` 化されていて、全体としては `https` のページが提供されている。そこに、`SSL` 未対応の `http` コンテンツが混じると具合が悪い。

そこで Let's Encrypt を利用して、動画サーバを `SSL` 化する。

手順については [**「CentOS8(nginx)にcertbotを使ってシンプルに無料SSL化＆SSL自動更新」**](https://qiita.com/matsuyoro/items/65fb8de2e7197e2ed472) を参照されたい。

上記の (1) ファイアウォール、(2) ルータのポート・マッピング、(3) DDNS の設定を済ませた上で、(4) NGINX を起動した状態で `certbot` を起動する。すると、証明書を取得すると同時に、`nginx.conf` に適切な変更を追加して、動画サーバを SSL 化してくれる。

``` nginx.conf
http {
    server {
        server_name  camsvr.on.ddns;
        listen 443 ssl; # managed by Certbot
        ssl_certificate /etc/letsencrypt/live/camsvr.on.ddns/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/camsvr.on.ddns/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    }
    server {
        if ($host = camsvr.on.ddns) {
            return 301 https://$host$request_uri;
        } # managed by Certbot
        listen 80;
        server_name  camsvr.on.ddns;
        return 404; # managed by Certbot
    }
}
```

`# managed by Certbot` とコメントされているところが `certbot` が書き換えたり書き加えたりしたところである。

#### 2-2-4-5 : CORS ヘッダ

すぐ上で述べたように、動画配信のウェブ・ページは、既存のウェブ・サーバが提供するページに、新設の動画サーバからのコンテンツが混じる形になる予定である。このとき `CORS (Cross-Origin Resource Sharing)` の問題が生じる。

これに対処するために、動画サーバからのレスポンスに CORS 対応のヘッダを追加する。

``` nginx.conf
http {
    server {
        location / {
            # CORS start
            add_header 'Access-Control-Allow-Origin' 'https://my.web.server' always;
            # Allow CORS preflight requests
            if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Origin' 'https://my.web.server';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Content-Type' 'text/plain charset=UTF-8';
                add_header 'Content-Length' 0;
                return 204;
            }
            # CORS end
        }
    }
}
```

身許を知っている特定のオリジンのみを許すという設定である、、、らしい。正直に言うと、私には CORS はよく判らない。とりあえず安全側に振り切った設定にしたつもりだ。

### 2-2-5 : NGINX による動画配信

[**"Enabling Video Streaming for Remote Learning with NGINX and NGINX Plus"**](https://www.nginx.com/blog/video-streaming-for-remote-learning-with-nginx/) を参考にして `nginx.conf` に動画配信の設定を追加する。

以下は `HLS` 用の設定である。

``` nginx.conf
http { 
    server { 
        location / { 
            root /tmp/hls; 
        } 
    }
    types {
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
        text/html html;
    } 
}
rtmp { 
    server { 
        listen 1935; 
        application livecam { 
            live on; 
            interleave on;
            hls on; 
            hls_path /tmp/hls;
            hls_fragment 1s;
            hls_playlist_length 10s;
        } 
    } 
} 
```

- `http` > `server` > `location /` の `root` を `/tmp/hls` に変更
  - `/var/www/livecam` とかでも構わない。
- `http` > `types` のブロックを追加設定
  - `html` に加えて、`HLS` 用の `m3u8` と `ts` のタイプを設定
- `rtmp` のブロックを追加設定
  - `hls_path` は `/tmp/hls` とする
    - 上記の `http` > `server` > `location /` > `root` と同じにする必要がある。
  - `hls_fragment` は個々のフラグメント (= `ts` ファイル) の長さ。1 秒とする。
  - `hls_playlist_length` はプレイリスト全体の長さ。10 秒とする。 

`http` 側の `location` > `root` の設定と `rtmp` 側の `hls_path` 設定を同じにする。この動画サーバは `URL` の `/` 直下で `HLS` 動画を配信する設定である。

`hls_fragment` と `hls_playlist_length` を短く設定すると再生遅延が小さくなる。ただし、ストリームのキー・フレーム間隔より短い `hls_fragment` を指定しても無視されるので、後述するように、ffmpeg による動画変換の際にキー・フレーム間隔を適切に設定する必要がある。

### 2-2-6 : ffmpeg による動画変換

最後に、ffmpeg によって IP カメラの RTSP ストリームを RTMP ストリームに変換して動画配信サーバ nginx に送りつける。

最も基本的な設定は次の通り。

```
$ ffmpeg -i rtsp://192.168.0.101/12.amp -c copy -f flv \
    rtmp://192.168.0.99/livecam/std
```

- シンタックスは、`ffmpeg [input options] -i inmput [output options] output`
- 入力
  - `-i rtsp://192.168.0.101/12.amp` ... IP カメラの RTSP ストリームを入力として指定。
    - `12` の部分は使用する IP カメラによって異なってくる。
    - SV3C SV-801W-1080P-HX は、`11`(1920 x 1080)  と `12`(640 x 352) の二つのストリームを提供している。ここでは後者を使用する。
- 出力オプション
  - `-c copy` ... 出力のコーデックは入力のままとする。
  - `-f flv` ... 出力形式の指定。FLV は、Flash Video。
- 出力
  - `rtmp://192.168.0.99/livecam/std` ... 出力先の指定。
    - `livecam` ... application 名。
    - `std` ... ストリーム名。
  - `RTMP` の URL は `rtmp://camsvr.on.ddns/livecam/std` となる。
  - `HLS` の URL は `https://camsvr.on.ddns/std.m3u8` となる。

動画を加工しなくて良い場合は出力オプションで `-c copy` が使える。入力のデコードと出力のエンコードの手間が省けるので、CPU の負担がほぼ皆無になる。

画像のサイズを変更したり、キー・フレーム間隔を指定したり、色合いを調整したりするとなると、もう少しややこしくなり、CPU の負担も大きくなる。

```
$ ffmpeg \
    -rtsp_transport tcp \
    -i rtsp://192.168.0.101/12.amp \
    -c:v libx264 \
    -b:v 512k \
    -r 15 \
    -x264-params keyint=30:scenecut=0 \
    -vf hue=h=10 \
    -c:a aac \
    -f flv \
    rtmp://192.168.0.99/livecam/std
```

- 入力オプション
  - `-rtsp_transport tcp` ... RTSP 通信を TCP で行う。
    - `-i` の入力指定より前に指定する必要がある。
    - `max delay reached. need to consume packet / RTP: missed xx packets` というエラーが頻発する場合に指定すると良いらしい。うちの環境では効果があった。
- 出力オプション
  - `-c:v libx264` ... ビデオ・コーデックの指定。H.264 である。
  - `-b:v 512k` ... ビデオのビット・レートの指定。512kb/sec。
  - `-r 15` ... ビデオのフレーム・レートの指定。15 fps。
  - `-x264-params keyint=15:scenecut=0` ... キーフレーム間隔の指定。15 フレーム、すなわち、1 秒。
    - HLS の `hls_fragment` 以下のキー・フレーム間隔を指定する必要がある。
  - `-vf hue=h=10` ... ビデオ・フィルタの指定。色相を 10 度回転。
  - `-c:a aac` ... オーディオ・コーデックの指定。

色相の調整は、この IP カメラの機種としての特性なのか、個体としての特性なのか、画像の赤みが強すぎるのを補正しようとするものだ。

##### iOS 上のブラウザに対応する (2024-01-30 追記)

Windows 上のブラウザでは上記の設定でよいのだが、iOS 上のブラウザでは動画がうまく再生出来ない場合がある。音声トラックの問題だ。**iOS は音声トラックの無い動画を再生してくれない。**

最初、出力オプションで `-an` (音声無し) を指定していた。ところが、2021年10月に iOS を 15 に上げた iPhone で動画再生が開始されるまでに非常に長い時間待たされるようになった。試しに `-an` の代りに `-c:a aac` を指定すると、すんなりと動画再生が開始されるようになった。iOS 15 が音声トラックを受信しようとして長い間リトライを繰り返していたらしい。

`-c:a aac` で一件落着と思っていたのだが、いつの間にか、また iOS で動画が再生されなくなった。原因は、私がネットワーク・カメラの設定をいじって音声出力を OFF にしたことだったのだが、その事に気が付くまで随分と長い月日を要した。今日、やっと気が付いた。ネットワーク・カメラの音声出力を ON にしたら、iOS 上のブラウザでも、元の設定で問題なく動画を再生することが出来た。

いろいろと試行錯誤した結果として、音声トラックにダミーの空入力を使うという方法があることを発見したので、参考のために記録しておく。

```
$ ffmpeg \
    -f lavfi \
    -i anullsrc=channel_layout=mono:sample_rate=24000 \
    -rtsp_transport tcp \
    -i rtsp://192.168.0.101/12.amp \
    -c:v libx264 \
    -b:v 512k \
    -r 15 \
    -x264-params keyint=30:scenecut=0 \
    -vf hue=h=10 \
    -c:a aac \
    -map 0:a \
    -map 1:v \
    -f flv \
    rtmp://192.168.0.99/livecam/std
```

- シンタックスは、`ffmpeg [input#0 options] -i inmput#0 [input#1 options] -i inmput#1 [output options] output`
  - 入力を2つ指定しているところに注目
- 入力#0 オプション
  - `-f lavfi` ... 入力#0のフォーマット。
- 入力#0
  - `-i anullsrc` ... 空のダミー入力。
    - `channel_layout=mono` ... モノラル音声。
    - `sample_rate=24000` ... サンプリング・レート。
- 出力オプション
  - `-map 0:a` ... 入力#0 の音声を使用する。
  - `-map 1:v` ... 入力#1 のビデオを使用する。

今となっては不要なトリックになったが、少し修正すれば、ネットワーク・カメラの音声を別のもの（BGMなど）に差し替えたい場合に使えると思うので、全く無駄な苦労をした訳でもないと自分を慰める次第である。

#### 2-2-6-1 : ffmpeg の監視 (obsoleted)

IP カメラの誤動作などで RTSP ストリームが乱れると ffmpeg が終了する場合がある。それでは動画変換サーバとして具合が悪いので、ffmpeg の動作を監視して止っていたら再起動するシェル・スクリプトを作成する。

```
#!/usr/bin/bash
# The script runs for 18 hours (12 x 60 x 18 = 12960)
limit=12960
counter=0
while [ $counter -le $limit ]; do
    sleep 5
    alive=`ps -aef | grep ffmpeg | grep livecam | wc -l`
    # echo `date` ":ffmpeg alive ="$alive
    if [ $alive = 0 ]; then
        echo `date` " : ffmpeg is not running."
        echo "starting ffmpeg ..."
        ffmpeg -f lavfi -i anullsrc=channel_layout=mono:sample_rate=24000 \
            -rtsp_transport tcp -i rtsp://192.168.0.101/12.amp \
            -c:v libx264 -b:v 512k -r 15 -x264-params keyint=30:scenecut=0 \
            -vf hue=h=10 -c:a aac  -map: 0:a -map 1:v-f flv \
            rtmp://192.168.0.99/livecam/std \
            2>>/var/log/nginx/ffmpeg-std.log &
        echo "done."
    fi
    ((counter++))
done
```

18時間にわたって、5秒ごとに ffmpeg の生存をチェックし、死んでいたら起動する。このスクリプトを `cron` で毎日 3:00 AM に起動する。9:00 PM から 3:00 AM の間は死んでいても良い、というズボラな設定である。

#### 2-2-6-2 : NGINX に ffmpeg を起動させる (also obsoleted)

実は上記のように自分で ffmpeg を起動して監視する必要は無い。NGINX に ffmpeg を起動させればよい。

```nginx.conf
        application livecam { 
            live on; 
            interleave on;
            hls on; 
            hls_path /tmp/hls; 
            hls_fragment 1s;
            hls_playlist_length 10s;
            exec_static /usr/bin/ffmpeg \
                -f lavfi -i anullsrc=channel_layout=mono:sample_rate=24000 \
                -rtsp_transport tcp -i rtsp://192.168.0.101/12.amp \
                -c:v libx264 -b:v 512k -r 15 -x264-params keyint=30:scenecut=0 \
                -vf hue=h=10 -c:a aac  -map: 0:a -map 1:v-f flv \
                rtmp://192.168.0.99/livecam/std \
                2>>/var/log/nginx/ffmpeg-std.log;
        } 
```

ここでは `exec_static` を使っているが、その理由は、何故か判らないが私の環境では `exec_push` が期待通りに ffmpeg を起動しない、ということにある。この障害は `nginx-rtmp-module` の github にも開いたままの issue として記録されており、大勢の人を悩ませているようである。

さいわい `exec_static` なら起動に成功する。そして起動された ffmpeg を殺しても、NGINX が監視しているのだろう、すかさず別のインスタンスが起動される。NGINX を終了させると ffmpeg も終了する。これで良い。

と思ったのだが、これではうまく行かない場合があった。**ffmpeg が死なずに止ってしまう** ことがある。つまり、`ps` で見ると生きているが、`top` で見ると全く働いていない状態に陥る場合がある。

#### 2-2-6-3 : ffmpeg の監視（再び）

ffmpeg を監視して、死んだり怠けたりしていたら NGINX ごと再起動をかけるスクリプトを走らせる。

```ffcheck.sh
#!/usr/bin/bash
# The script runs for 14 hours (60 x 60 x 14 = 50400) by default
LIMIT=50400
if [ $# -ge 1 ]; then
^Ilet LIMIT=$1
fi
echo "Going to check ffmpeg for $LIMIT seconds ..." 1>&2
# counter
DYING=0
# limit to restart nginx
DYING_LIMIT=5
$
while [ $I -lt $LIMIT ];
do
  echo -n "`date +"%m-%d %H:%M:%S"` " 1>&2
  TOPINFO=`top -b -n 1 | grep ffmpeg`
  INFO=($TOPINFO)
  if [ "$INFO" = "" ]; then
    let DYING++
    echo "--- warning $DYING." 1>&2
  else
    CPU=${INFO[8]}
    CPUX=${CPU/./}
    if [ $CPUX -ge 50 ]; then
      let DYING=0
      echo "$CPU" 1>&2
    else
      let DYING++
      echo "$CPU warning $DYING." 1>&2
    fi
  fi
  if [ $DYING -ge $DYING_LIMIT ]; then
    echo "Restarting nginx ..." 1>&2
    systemctl restart nginx
    let DYING=0
    sleep 3
  fi
  sleep 1
  let I++
done
echo "Finished." 1>&2
```

1秒ごとに `top` で `ffmpeg` の情報の取得を試みて、情報が取得できなかったり、CPU 使用率が 5% を割っている状態が5秒続いたら、NGINX を再起動する。

まあ、何とも荒っぽい事であり、識者から失笑を買いそうである。

現在は、このスクリプトを `cron` で毎日 05:00 に起動している。

```/etc/cron.d/ffcheck
0 5 * * * root /root/ffcheck.sh &> /var/log/nginx/ffcheck.log
```


## 2-3 : クライアント側

### 2-3-1 : RTMP ストリームの再生

**VLC** や **MPC-HC** などのプレーヤーでは、URL `rtmp://camsvr.on.ddns/livecam/std` を開くことによって、**RTMP** ストリームを再生することが出来る。

### 2-3-2 : ウェブ・ページ

ウェブ・ページでは JavaScript の動画再生ライブラリ **VIDEO JS** を使用して、**HLS** ストリームを再生するのが楽である。

```html
<head>
    <link href="https://vjs.zencdn.net/8.3.0/video-js.min.css" rel="stylesheet" />
</head>

<body>
    <video id="live-cam" class="video-js" controls autoplay
            width="640" height="352" data-setup="{}">
        <source src="https://camsvr.on.ddns/std.m3u8" type="application/x-mpegURL" />
        <p class="vjs-no-js">
            この動画を見るためには JavaScript を有効にし、
            HTML5 の video タグをサポートするブラウザを使って下さい。
        </p>
    </video>

    <script src="https://vjs.zencdn.net/8.3.0/video.min.js"></script>
</body>
```

詳細は、[**VIDEO JS 公式サイト**](https://videojs.com/) の [**"Getting Started"**](https://videojs.com/getting-started) およびドキュメントを参照されたい。

## 2-4 : YouTube への配信

YouTube にライブ配信する場合は次のようにする。

```nginx.conf
        application youtube { 
            live on; 
            interleave on;
            exec_static /usr/bin/ffmpeg -rtsp_transport tcp \
                -i rtsp://192.168.0.101/12.amp -c:v libx264 -r 15 -s 640x360 -b:v 512k \
                -c:a aac -f flv rtmp: //192.168.0.99/youtube/you-tube-stream-key \
                2>>/var/log/nginx/ffmpeg-youtube.log;
            push rtmp://a.youtube.com/live2/you-tube-stream-key;
        }
```

- `-s 640x360` ... 640x352 のサイズは受け付けてくれない。ちょっと手抜き。`scale` と `pad` を設定する方が良い。
- `-c:a aac` ... YouTube は音声無しのストリームを受け付けてくれない。

音声無しのストリームが駄目だということに気付かず、三日間悩んだ。

YouTube 配信の場合、`application` を開いておいて、必要なときだけ ffmpeg を起動するというのでも良いかも知れない。

# 3 : 終りに

以上で IP カメラの動画をウェブ・ページで公開するという仕事は終る。

使用したフリー・ソフトウェア（オープン・ソース・ソフトウェア）の作者たちと、そのノウ・ハウをウェブ上に公開している人たち（当記事からリンクを張っているもの限らず、数多くの文書を参照させてもらった）に心から感謝する。

読んで下さった方には、何か間違っている所やおかしな所、改善すべき所があれば、是非ともご教示下さるようにお願いする。