配信中継サーバ開発2:ffmpegを用いたyoutubeへのテスト配信とローカルデバッグ環境の構築
技術スタック DockerNginx

配信中継サーバ開発2:ffmpegを用いたyoutubeへのテスト配信とローカルデバッグ環境の構築

2024.07.09

こんにちはjunです。前回の記事から2年も経ってしまいましたが、サイクリング欲が再度湧いてきたのでこの内容を完成させるように頑張ります。ではとりあえず前回はgoproからdockerのRTMPサーバに送信してローカルマシン上で見れるかを確かめました。最終的には以下のような構成を開発します。(その1のものと変わっています)

今回の記事は

  • 上記図の環境をローカルでdockerで構築する
  • 管理用サーバ上のffmpegを用いてyoutubeにgoproの映像を中継してライブする
  • ローカルで簡単に配信をデバッグできる環境を構築する

の3点を解説したいと思います。

これらの内容をまずローカルで実装するためにDockerを用いて構築します。使用しているOSはmacOs Ventura 13.4.1でdockerは4.17.0です。また前回から2年たっているのでdockerfileなどを修正します。

完成品

この記事でのゴールはこんな感じです。(字幕ON推奨)

全ファイル構成

とりあえず完成した必要なファイルと構成を載せます。

.
├── Dockerfile
├── conf
│   ├── 000-default.conf
│   ├── default.conf
│   └── nginx.conf
├── debug
│   └── index.html
├── docker-compose.yml
└── src
    └── public
        └── test.html

Dockerfileは管理用コンテナの構成に変更。あとでwebフレームワークのlumenを入れるためにcomposerを入れています。(この回では入れません)。そしてffmpegを入れておきます。

FROM php:apache
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN curl -O https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
    tar -xJf ffmpeg-release-amd64-static.tar.xz && \
    mv ffmpeg-*/ffmpeg /usr/local/bin/ && \
    mv ffmpeg-*/ffprobe /usr/local/bin/ && \
    rm -rf ffmpeg-*
  • rtmptargetがテスト配信先コンテナ
  • rtmpがGorpoなどからの受信用コンテナ
  • phpが管理用web uiや配信媒体へ送信する処理を行うコンテナ
docker-compose.yml
version: '2'
services:
  rtmptarget:
    image: tiangolo/nginx-rtmp
    ports:
      - "1930:1935"
      - "1931:80"
    volumes:
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
      - ./conf/default.conf:/etc/nginx/conf.d/default.conf
      - ./debug:/usr/share/nginx/
  rtmp:
    image: tiangolo/nginx-rtmp
    ports:
      - "1935:1935"
    volumes:
      - shared_data:/usr/share/nginx/hls
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
  php:
    build: .
    expose:
      - 1935
    ports:
      - "8080:80"
    volumes:
      - shared_data:/var/www/html/hls
      - ./src:/var/www/html
      - ./conf/.htpasswd:/var/www/.htpasswd
      - ./conf/000-default.conf:/etc/apache2/sites-enabled/000-default.conf
    extra_hosts:
      - "host.docker.internal:host-gateway"
volumes:
  shared_data:

主にRTMPサーバ用の内容です。

conf/nginx.conf
worker_processes  auto;

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


events {
    worker_connections  1024;
}

rtmp_auto_push on;

rtmp {
    server {
        listen 1935;
        listen [::]:1935 ipv6only=on;
        access_log /var/log/rtmp_access.log;
        chunk_size 4096;
        timeout 10s;

        application live {
            live on;

            # HLSの記述欄
            hls on;
            # ここに映像ファイルが配置される
            hls_path /usr/share/nginx/hls;
            hls_fragment 10s;
            hls_playlist_length 30s;
        }
    }
}



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;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

主にRTMPサーバ用の内容です。

conf/default.conf
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

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

    location / {
        root   /usr/share/nginx;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

管理用コンテナに使用します。ドキュメントルートが/var/www/html/publicなのは後でlumenを入れるためです。

conf/000-default.conf
<VirtualHost *:80>
    # The ServerName directive sets the request scheme, hostname and port that
    # the server uses to identify itself. This is used when creating
    # redirection URLs. In the context of virtual hosts, the ServerName
    # specifies what hostname must appear in the request's Host: header to
    # match this virtual host. For the default virtual host (this file) this
    # value is not decisive as it is used as a last resort host regardless.
    # However, you must set it for any further virtual host explicitly.
    #ServerName www.example.com

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/public

    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    # error, crit, alert, emerg.
    # It is also possible to configure the loglevel for particular
    # modules, e.g.
    #LogLevel info ssl:warn

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    # For most configuration files from conf-available/, which are
    # enabled or disabled at a global level, it is possible to
    # include a line for only one particular virtual host. For example the
    # following line enables the CGI configuration for this host only
    # after it has been globally disabled with "a2disconf".
    #Include conf-available/serve-cgi-bin.conf
</VirtualHost>

とりあえず確認したい時に使用します。

src/public/test.html,debug/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>MediaElement</title>
  <!-- MediaElement style -->
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/mediaelement/4.2.9/mediaelementplayer.css" />
</head>

<body>
  <!-- MediaElement -->
  <script src="//cdnjs.cloudflare.com/ajax/libs/mediaelement/4.2.9/mediaelement-and-player.js"></script>

  <video id="player" width="640" height="360">
</body>
<script type="text/javascript">

      var player = new MediaElementPlayer('player', {
        success: function(mediaElement, originalNode) {
          console.log("Player initialised");
        }
      });
        player.setSrc("/hls/.m3u8");
</script>

</html>

では上記のファイルを用いて解説します。

共有ボリュームを用いて受信した映像を扱う

まずは受信用コンテナから得られたGoproの映像を管理用コンテナで使用できるようにします。受信用コンテナにPHPなどを入れようとしましたが結構面倒だったので、別のコンテナとして独立させました。しかしそのままでは受信した映像データ(HLS)を共有できないため、受信用コンテナで生成されるHLSファイルを共有ボリュームで共有します。

その設定のために

docker-compose.yml
version: '2'
services:
  #...
  rtmp:
    #...
    volumes:
      - shared_data:/usr/share/nginx/hls
    #...
  php:
    #...
    volumes:
      - shared_data:/var/www/html/hls
    #...
volumes:
  shared_data:

volumesでボリュームを定義してそのボリューム名を各コンテナに配置するパスを指定します。するとrtmp(受信用コンテナ)では/usr/share/nginx/hls*.tsファイルや.m3u8ファイルが作成されます。そしてphp(管理用コンテナ)では/var/www/html/hlsに同じファイル群が作成されます。

管理用コンテナでgoproから受信したデータをffmpegで色々したり、読み取ったり、配信できるようになりました。

テストしてみる

その1の記事または後述するOBSの方法を参考にGopro・OBSからrtmp://YOUR_PC_IP/live にRTMP送信をします。

# <YOUR_PHP_CONTAINER_NAME>はあなたの環境の管理用コンテナ名に書き換えてください。

docker exec -it <YOUR_PHP_CONTAINER_NAME> /bin/bash

ボリュームのため、/var/www/html/hlsというディレクトリがあると思います。そしてGoproでの収録を開始するとディレクトリ内にファイルが配置されます。

そして/var/www/html/public/test.htmlがあるのでhttp://localhost:8080でweb playerを用いて確認できるはずです。

ffmpegを用いてYoutubeに送信してみる

次にffmpegを用いて管理用コンテナからyoutube liveに流してみます。ffmpegはlinux上で動画、音声の変換・記録・再生・配信を行うことができるOSSです。今はコマンド上で配信をするだけですが、最終的にはffmpegを利用して管理画面から配信内容を制御する予定です。現時点で自分もまだ詳細なプロパティーや映像系の知識に乏しいので、調べた際のコマンドのコピペやchat gptで調査した内容になります。

youtube のライブを始めておく

本記事では簡単に述べておきます。すでにGoogle、Youtubeアカウントがありライブストリーミングが有効になっている前提とします。

  1. Youtubeに移動し、右上の「+」ボタンをクリックして「ライブ配信を開始」を選択
  2. ライブ配信の設定からストリームURLとストリームキーを控えます。ライブストリームキーとURLは「rtmp://a.rtmp.youtube.com/live2/LIVE_KEY」とURLの後につければOKです。ただしドメインのとこはyoutubeのrtmp配信サーバのipにしておいてください(調べ方と理由は後述)
  3. Youtubeのライブ管理画面では「ライブ配信を公開するには、ストリーミング ソフトで動画の送信を開始します」という表示がされています。ffmpegでRTMP送信をすれば表示されます。

プライバシーを非公開にしておくことをおすすめします。

コマンドを打つ

Goproからの映像をローカルで確認できたらとりあえず以下のコマンドを管理用コンテナ内から打ってみてください。

ffmpeg -re -i "/var/www/html/hls/.m3u8" -c:a aac -ar 44100 -ab 128k -ac 2 -strict -2 -flags +global_header -bsf:a aac_adtstoasc -f flv "rtmp://64.233.189.134/live2/YOUR_LIVE_KEY"

YOUR_LIVE_KEY はyoutuebのライブストリームキーを使用してください。ffmpegが起動してyoutubeにデータが送信されます。20秒ぐらいするとGoproからの映像の配信が開始されるはずです。

DNSエラー?

本来ライブストリームURLにはa.rtmp.youtube.comというドメインを使用すべきですが、私のdockerで管理用コンテナから実行するとffmpegが以下のようなDNSエラーが発生します。

[tcp @ 0x696d2c0] Failed to resolve hostname a.rtmp.youtube.com: System error
[rtmp @ 0x696d480] Cannot open connection tcp://a.rtmp.youtube.com:1935?tcp_nodelay=0
[out#0/flv @ 0x6890880] Error opening output rtmp://a.rtmp.youtube.com/live2/efth-usbu-ye2g-f0sk-a7ta: Input/output error
Error opening output file rtmp://a.rtmp.youtube.com/live2/efth-usbu-ye2g-f0sk-a7ta.
Error opening output files: Input/output error

ホストマシン上でffmpegを使用すると問題なく配信できるますが、docker内から行うとなぜかa.rtmp.youtube.comのホストが見つからないというエラーが発生します。そのためホストマシンでnslookupを使用してa.rtmp.youtube.comのIPを調べて、直接IPを指定したら問題なく動作しました。

ここはdockerの問題かつまだ調査中です。Googleなどはこの辺のIPをころころ変えてきそうなので、最終的にはきちんとドメインを利用できるようにしますがとりあえず今はIPで指定します。

ローカルでデバッグしやすい環境にする

ひととおり受信→中継→配信を行えるdockerコンテナが構築できました。しかし毎回goproを起動したり、youtube liveを開始するのは面倒ですし、複数人で開発する時に機材がなかったり送信先が重複してします。そのため開発用にデバッグできるように送信元、配信先をローカルで実現できるようにします。

送信元にgoproの代わりにOBSを利用する

この記事では送信元としてGoproを使用してホストマシンの受信用コンテナにRTMPを送信しています。ですがこれはOBSなどの配信ソフトを利用することで簡単に代替できます。

OBSはフリーのライブ配信や録画が行えます。PC上のウィンドウキャプチャをしたり、機材をそろえればnintedo switchなどのゲーム機の映像を配信することができます。とりあえずOBSをインストールして、webカメラなりウィンドウキャプチャを映像・音声ソースとして利用します。OBSのインストールはこちらから

とりあえずmacの映像と音声を受信用RTMPに流そう

OBSをインストールしたらアプリを開きます。以下のような似た画面になっているはずです。(すでに設定がいくつかあるのは気にしないでください)

「シーン」に何もなければ「+」をクリックしてシーンを追加します。そして映像・音声ソースも追加していきます。「ソース」のとこの「+」をクリックして「映像キャプチャデバイス」を選択します。

ソースのラベルを適当に入れておきます。

ソースの選択画面に移動するので「デバイス」から「FaceTime HDカメラ」などを選択しておきます。

これだとまだ映像しか拾わないので次は「ソース」のとこの「+」をクリックして「音声入力キャプチャ」を選択します。同じようにソースのラベルを適当に入れておきます。ソースの選択画面に移動するので「デバイス」から「既定」などを選択しておきます。どれでもとりあえず音声が取得できればOKです。

こんな感じで映像となんか奇声を発して「音声入力キャプチャ」が反応していればOKです。

次に配信の設定をします。OBS→「設定」を開いて配信のタブをクリックします。 サービスから「カスタム」を選択します。

そして「サーバー」に受信用コンテナのRTMPパスをいれます。このとき127.0.0.1というローカルループバックアドレスにしておきます。localhostにするとうまくいかない時があります。自分はlocalhost:1935に受信用コンテナのエンドポイントが控えていますが、違うポートの時はそのポートを指定してください。「適用」をクリックして「OK」をクリックします。

最初の画面にもどったら「配信開始」をクリックすると配信されます。下の方にこのような表示がされていたり、確認用playerで確認したり、HLSファイルが生成されていれば成功しています。

終了する時は「配信終了」をクリックします。

配信先にローカルの別のRTMPサーバを使用する。

最後に配信先のRTMPサーバをローカルに用意します。一番簡単な方法としてはもう1つRTMPサーバを立ててしまうことです。気をつける点としては管理用コンテナから配信先のホストを指定できるようにします。

docker-compose.yml
version: '2'
services:
  rtmptarget:
    image: tiangolo/nginx-rtmp
    ports:
      - "1930:1935"
      - "1931:80"
    volumes:
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
      - ./conf/default.conf:/etc/nginx/conf.d/default.conf
      - ./debug:/usr/share/nginx/
    # ...
  php:
    # ...
    extra_hosts:
      - "host.docker.internal:host-gateway"
    # ...
volumes:
  shared_data:

管理用コンテナのプロパティにextra_hostshost.docker.internal:host-gatewayを加えることで、コンテナから利用できるホストマシンの特殊なIPアドレスがコンテナのhostsに追記されます。

cat /etc/hostsを管理用サーバで実行すると追加したhostのIPがあるはずです。

127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
192.168.65.2    host.docker.internal ←これ
172.23.0.3      8227ce3421ec

ffmpegを実行する

次に配信先のRTMPコンテナに向けてffmpegを用いて配信します。ローカルのホスト、ポート、パスに合わせればいいだけですが以下のコマンドを使用します。(youtubeの時とすこし違います!!)

ffmpeg -re -i "/var/www/html/hls/.m3u8" -c:v libx264 -preset ultrafast -c:a aac -ar 44100 -ab 128k -ac 2 -f flv "rtmp://192.168.65.2:1930/live"

このコマンドを打って配信先コンテナの確認用htmlのプレイヤーhttp://localhost:1931/index.htmlから映像を確認できれば受信できています。(20秒ほど待つといいです)

すこしオプションの内容がyoutubeのときと違っている理由として、単純にyoutubeのIPから変えればいいと思っていたらなぜかweb playerで映像が見れず音声しか流れなかったのが原因です。色々探ってこのコマンドにしたら問題なくいけました。もしかするとIPだけ変更してyoutubeもこのコマンドでやったらほうがいいかもしれません。検証中です。

まとめと参考文献

記事は以上となります。一通り管理用の中継コンテナを用いてgoproの映像をffmpegでyoutubeに送信したり、ローカルの開発環境を整えました。次回はffmpegを用いて任意のシーンに切り替えたり、goproからの映像が途絶えた時などの例外処理を加えていくwebシステム部分をphpを使用して実装しようと思います。

今回参考にした資料です。

コメント

コメント読み込み中..

Copyright © 2021 jun. All rights reserved.