nginxを使ってRTMPSとHLS再生を1コンテナで実現する（nginx + stream + rtmp）

ある事情で[RTMP](https://ja.wikipedia.org/wiki/Real_Time_Messaging_Protocol)による映像配信基盤が必要になりそうになったため [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) を使って自前で組んでいる際中のメモ書き的な記事になります。

# 実現内容

この記事で作る構成は以下の通りです。

![nginx-rtmp.drawio (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819924/4f0b9379-6611-4a59-4caf-dfd9d5db9758.png)

記事のタイトルの通り、１コンテナで映像伝送と映像配信を両方行えるようにしています。

まずは映像の送信側です。
OBSやMicrosoft Teamsから443ポートに向けて映像伝送を行います。その際、RTMPSで盗聴・改ざんを防止します。映像はまずnginxの[steramモジュール](http://nginx.org/en/docs/stream/ngx_stream_core_module.html)で受信し、SSL通信を終端します。次に[SNI](https://ja.wikipedia.org/wiki/Server_Name_Indication)で転送先のバックエンドを選択し、最終的にnginxのRTMPモジュールに送られ、HLSファイル（`*.m3u8`, `*.ts`）に保存されます。

次に映像の受信側です。
Webブラウザで、同じく443ポートにHTTPSでアクセスします。nginxのstreamモジュールでSSL通信を終端し、SNIでHTTPのバックエンドへ転送します。HTTPのバックエンドからHLSファイル群やHTMLプレイヤーを読み込むことで映像を再生します。

この構成により、OBSやMicrosoft Teamsのような、RTMPによる映像伝送機能を持ったクライアントを使って、遠隔地へ映像を届けることができます。

## stunnel について

nginxとRTMPSのキーワードで調べると、[stunnel](https://www.stunnel.org/)を使ってOBSやMicrosoft TeamsからのSSLを終端する構成が見受けられましたが、nginxのstreamモジュールを使えば同じくSSL終端できる上に、nginxのアクセス制御の恩恵にあずかれることと設定を集約できることから、最終的にはstunnelを使用していません。

## nginx の Dockerfile

Dockerfileは[tiangolo/nginx-rtmp](https://hub.docker.com/r/tiangolo/nginx-rtmp/)のイメージを少し修正しています。こちらのイメージにはnginxのstreamモジュールが含まれていなかったため `configure` のオプションに `--with-stream` と `--with-stream_ssl_module` を追加しました。

```dockerfile:Dockerfile
FROM buildpack-deps:bullseye

# Versions of nginx and nginx-rtmp-module to use
ENV NGINX_VERSION nginx-1.23.2
ENV NGINX_RTMP_MODULE_VERSION 1.2.2

# Install dependencies
RUN apt-get update && \
    apt-get install -y ca-certificates openssl libssl-dev && \
    rm -rf /var/lib/apt/lists/*

# Download and decompress nginx
RUN mkdir -p /tmp/build/nginx && \
    cd /tmp/build/nginx && \
    wget -O ${NGINX_VERSION}.tar.gz https://nginx.org/download/${NGINX_VERSION}.tar.gz && \
    tar -zxf ${NGINX_VERSION}.tar.gz

# Download and decompress RTMP module
RUN mkdir -p /tmp/build/nginx-rtmp-module && \
    cd /tmp/build/nginx-rtmp-module && \
    wget -O nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION}.tar.gz https://github.com/arut/nginx-rtmp-module/archive/v${NGINX_RTMP_MODULE_VERSION}.tar.gz && \
    tar -zxf nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION}.tar.gz && \
    cd nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION}

# Build and install nginx
# The default puts everything under /usr/local/nginx, so it's needed to change
# it explicitly. Not just for order but to have it in the PATH
RUN cd /tmp/build/nginx/${NGINX_VERSION} && \
    ./configure \
        --sbin-path=/usr/local/sbin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --pid-path=/var/run/nginx/nginx.pid \
        --lock-path=/var/lock/nginx/nginx.lock \
        --http-log-path=/var/log/nginx/access.log \
        --http-client-body-temp-path=/tmp/nginx-client-body \
        --with-http_ssl_module \
        --with-threads \
        --with-ipv6 \
        --with-stream \
        --with-stream_ssl_module \
        --add-module=/tmp/build/nginx-rtmp-module/nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION} --with-debug && \
    make -j $(getconf _NPROCESSORS_ONLN) && \
    make install && \
    mkdir /var/lock/nginx && \
    rm -rf /tmp/build

# Forward logs to Docker
RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
    ln -sf /dev/stderr /var/log/nginx/error.log

# Set up config file
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 1935
CMD ["nginx", "-g", "daemon off;"]
```

## 証明書の取得

今回は[Let's Encrypt](https://letsencrypt.org/ja/)から証明書を取得しました。
細かい手順は省きますが、備忘録も兼ねて証明書を取得した時に使用した `docker-compose.yml` を載せておきます。

```yaml:docker-compose-certbot.yml
version: "3"
services:
  certbot:
    image: certbot/certbot:latest
    volumes:
      - type: bind
        source: ./data/certbot/etc/letsencrypt
        target: /etc/letsencrypt
      - type: bind
        source: ./data/certbot/var/lib/letsencrypt
        target: /var/lib/letsencrypt
    command: ["certonly", "--manual", "--agree-tos", "--no-eff-email", "--preferred-challenges", "dns"]
```

実行コマンドは以下の通りです。

```bash
docker compose run -f docker-compose-certbot.yml --rm certbot
```

今回はワイルドカード証明書を取得するため `--preferred-challenges` は `dns` に設定しています。
また、ローカルでテストを行ったため名前の向き先は内部IPアドレスになります。
パブリックDNSに登録するレコードのサンプルは以下の通りです。`example.com` 部分はダミーですので、取得したドメインに合わせて変更します。

|名前|タイプ|値|
|-|-|-|
|*.local.live.example.com|A|127.0.0.1|
|_acme-challenge.local.live.example.com|TXT|certbotから指示される文字列|

# 構築

では、構築手順について説明します。今回は、GitHub等でファイルを公開していないので、この記事に書いてあることが今お見せできる内容の全てになります。

## 使用環境

- Windows 10
- Docker Desktop for Windows
- WSL2
- OBS 29.1.2

## ファイル構成

Docker を実行する端末の中に以下の様なファイル構成を作ります。

```
- data/
  - nginx/
    - hls/  # HLSファイル保存場所
- deployments/
  - nginx/
    - certs/
      - certificate.crt  # Let's Encrypt 証明書
      - private.key  # Let's Encrypt 秘密鍵
  - etc/
    - nginx/
      - nginx.conf
  - www/
    - index.html  # ビデオプレイヤー
- images/
  - nginx/
    - Dockerfile
    - nginx.conf  # イメージビルド用nginx設定
- docker-compose-certbot.yml
- docker-compose.yml
```

## docker-compose.yml

今回テストで使用した `docker-compose.yml` はこちら。

```yml:docker-compose.yml
version: "3"
services:
  nginx:
    build:
      context: ./images/nginx
    volumes:
      - type: bind
        source: ./deployments/nginx/certs
        target: /certs
      - type: bind
        source: ./deployments/nginx/etc/nginx/nginx.conf
        target: /etc/nginx/nginx.conf
      - type: bind
        source: ./deployments/nginx/www
        target: /www
      - type: bind
        source: ./data/nginx/hls
        target: /hls
    ports:
      - "80:80"
      - "443:443"
      - "1935:1935"
```

ポート番号 `80` と `1935` を公開していますが、テスト用なので本番デプロイ時は不要です。

## nginx.conf

今回使用したnginxの設定ファイルはこちら。

```nginx:nginx.conf
worker_processes  auto;

error_log  /var/log/nginx/error.log  info;
pid        /var/run/nginx.pid;

rtmp_auto_push  on;

events {
    worker_connections  1024;
}

rtmp {
    server {
        listen  1935;
        listen  [::]:1935  ipv6only=on;

        application live {
            live    on;
            record  off;

            hls           on;
            hls_path      /hls;
            hls_fragment  10s;
        }
    }
}

stream {
    map  $ssl_server_name  $backend {
        www.local.live.example.com   127.0.0.1:80;
        rtmp.local.live.example.com  127.0.0.1:1935;
    }

    server {
        listen  443  ssl;
        listen  [::]:443  ssl  ipv6only=on;

        ssl_prefer_server_ciphers  on;
        ssl_certificate            /certs/certificate.crt;
        ssl_certificate_key        /certs/private.key;
        ssl_protocols              TLSv1.2 TLSv1.3;
        ssl_ciphers                ECDHE:HIGH:!aNULL:!MD5;

        proxy_pass  $backend;
    }
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile    on;
    tcp_nopush  on;

    gzip  off;

    keepalive_timeout  65;

    server {
        listen  80;

        location /hls/ {
            alias /hls/;
        }

        location / {
            alias /www/;
        }
    }
}

```

例によって `example.com` の部分はダミーですので、自身の持つドメインに合わせて変える必要があります。

この設定の全体像を要約すると以下のようになります。

- stream
  - `443` ポートで待ち受け
  - SSLを終端
  - SNIの名前に基づいて `80` か `1935` ポートに振り分ける
- rtmp
  - `1935` ポートで待ち受け
  - 映像を受信するとHLSファイルを保存
- http
  - `80` ポートで待ち受け
  - HLSファイルとビデオプレイヤーを公開

## ビデオプレイヤーHTML

HLSを再生するビデオプレイヤーのHTMLは以下の通りです。今回は [Video.js](https://videojs.com/) を使用しました。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Live</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://vjs.zencdn.net/8.3.0/video-js.css" rel="stylesheet" />
    <style>
        body {
            margin: 0;
            padding: 0;
        }
    </style>
  </head>
  <body>
    <div style="height: 100vh">
      <video
        autoplay
        id="player"
        class="video-js"
        style="width: 100%; height: 100%;"
        controls
        muted
        preload="auto"
        width="640"
        height="360"
        data-setup="{}">
        <source src="/hls/hogehoge.m3u8" type="application/vnd.apple.mpegurl" />
      </video>
    </div>
    <script src="https://vjs.zencdn.net/8.3.0/video.min.js"></script>
  </body>
</html>
```

`hogehoge.m3u8` というファイル名が書かれていますが、この中の `hogehoge` という部分がOBSやMicrosoft Teamsへ入力するストリームキーにあたります。

# 実行

諸々を構成したらサーバを実行します。

```bash
$ docker compose up
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: using the "epoll" event method
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: nginx/1.23.2
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6) 
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: OS: Linux 5.10.102.1-microsoft-standard-WSL2   
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576      
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker processes
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker process 7
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker process 8
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker process 9
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker process 10
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker process 11
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start worker process 12
rtmp-server-nginx-1  | 2023/07/27 04:58:21 [notice] 1#1: start cache manager process 13
```

次にOBSの配信設定を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819924/dc550a1a-37f9-d6cb-9751-87dc35510cee.png)

| 名前 | 値 |
|-|-|
|サービス|カスタム|
|サーバー|rtmps://rtmp.local.live.example.com/live|
|ストリームキー|hogehoge|

OBSで配信を開始したら、最後にビデオプレイヤーを `https://www.local.live.example.com/` から表示して、映像が再生されていればOKです。

# まとめ

nginx + stream + rtmp の組み合わせを説明した記事が無さそうだなぁ、ということで今回まとめてみました。
nginx-rtmp-module を使うと「Microsoft 365 の認証基盤で認証したユーザのみが配信、視聴できる」みたいなことも簡単にできそうなので、いずれ試してみるかもしれません。