📚

簡単ストリーミング中継サーバの作り方

2023/10/18に公開

TL;DR

  • OBS -> nginx -> web brouwser といった流れでストリーミングする
  • dockerでnginxは立てる。
  • 細かいところは省きます。

docker

nginxはRTMPが含まれているこちらを使っている。
https://hub.docker.com/r/alqutami/rtmp-hls
このdocker自体はffmpegによってマルチバンドのストリーミングを実現するような高機能な内容になってる。ひとまずこれのReadme読んで立ち上げてみるとOBSからの配信をウェブで見ることができます。

だがしかし。

ffmpegがとにかく落ちまくる。安定させるために色々と調べて手を尽くしたけども一向に安定する気配を見せなかった。で、いろいろ調べているうちに「nginx自体で受け取ったストリーミングデータをhlsに変換することができる」ということがわかったのでそちらに舵を切った。今のネット環境超早いので。

nginx.conf

dockerに設定されている当該のRTMPのコンフィグはこちら。

# RTMP configuration
rtmp {
    server {
        listen 1935; # Listen on standard RTMP port
        chunk_size 4000;
        # ping 30s;
        # notify_method get;

        # This application is to accept incoming stream
        application live {
            live on; # Allows live input

            # for each received stream, transcode for adaptive streaming
            # This single ffmpeg command takes the input and transforms
            # the source into 4 different streams with different bitrates
            # and qualities. 
            # these settings respect the aspect ratio.
            exec_push  /usr/local/bin/ffmpeg -i rtmp://localhost:1935/$app/$name -async 1 -vsync -1
                        -c:v libx264 -c:a aac -b:v 256k  -b:a 64k  -vf "scale=480:trunc(ow/a/2)*2"  -tune zerolatency -preset superfast -crf 30 -f flv rtmp://localhost:1935/show/$name_low
                        -c:v libx264 -c:a aac -b:v 768k  -b:a 128k -vf "scale=720:trunc(ow/a/2)*2"  -tune zerolatency -preset superfast -crf 30 -f flv rtmp://localhost:1935/show/$name_mid
                        -c:v libx264 -c:a aac -b:v 1024k -b:a 128k -vf "scale=960:trunc(ow/a/2)*2"  -tune zerolatency -preset superfast -crf 30 -f flv rtmp://localhost:1935/show/$name_high
                        -c:v libx264 -c:a aac -b:v 1920k -b:a 128k -vf "scale=1280:trunc(ow/a/2)*2" -tune zerolatency -preset superfast -crf 30 -f flv rtmp://localhost:1935/show/$name_hd720
                        -c copy -f flv rtmp://localhost:1935/show/$name_src > /tmp/ffmpeg.log 2>&1;
            drop_idle_publisher 10s;
        }

        # This is the HLS application
        application show {
            live on; # Allows live input from above application
            deny play all; # disable consuming the stream from nginx as rtmp

            hls on; # Enable HTTP Live Streaming
            hls_fragment 3;
            hls_playlist_length 20;
            hls_path /mnt/hls/;  # hls fragments path
            # Instruct clients to adjust resolution according to bandwidth
            hls_variant _src BANDWIDTH=4096000; # Source bitrate, source resolution
            hls_variant _hd720 BANDWIDTH=2048000; # High bitrate, HD 720p resolution
            hls_variant _high BANDWIDTH=1152000; # High bitrate, higher-than-SD resolution
            hls_variant _mid BANDWIDTH=448000; # Medium bitrate, SD resolution
            hls_variant _low BANDWIDTH=288000; # Low bitrate, sub-SD resolution

            # MPEG-DASH
            dash on;
            dash_path /mnt/dash/;  # dash fragments path
            dash_fragment 3;
            dash_playlist_length 20;
        }
    }
}

ffmpeg で色々やってるのがわかる。

で、これをこう変更した。

# RTMP configuration
rtmp {
    server {
        listen 1935; # Listen on standard RTMP port
        chunk_size 4000;

        application live {
            live on;
            drop_idle_publisher 10s;
            
            hls on;
            hls_fragment 3;
            hls_playlist_length 20;
            hls_path /mnt/hls/;
            hls_variant _mid BANDWIDTH=448000;

            dash on;
            dash_path /mnt/dash/;
            dash_fragment 3;
            dash_playlist_length 20;
        }
    }
}

これだけ。
http側のコンフィグは基本的に変更する必要ないのでそのままで。

TIPS

dockerの起動コマンド

nginxのコンフィグファイルとhtmlのドキュメントルートになるディレクトリの変更を行いたかったので、以下のようにしている。

docker run --name <起動後のプロセス名> -d -p 1935:1935 -p 80:8080 -v /home/<user_dir>/nginx_conf/nginx.conf:/etc/nginx/nginx.conf -v /home/<user_dir>/html:/usr/local/nginx/html --cpus=3 alqutami/rtmp-hls

-vで、コンフィグファイルやディレクトリをマウントして書き換えている。

結果

OBSから配信してるストリーミングの内容そのままが中継されるようになった。なので、OBS側の配信設定がそのままストリーミングに反映されてしまうので、解像度などは良きに計らうと良いと思う。ffmpegの時にはあまりに頻繁に落ちるので、スクリプトを作ってプロセスの監視を行い、都度dockerのrestartをする、という涙ぐましいその場対応してたのだが、いまのところ30時間以上連続してなにも問題なく配信が続いてるのでこれで良いかな、、、という気持ち。

やってみてあらためて実感したのだが、YouTubeとかマジでとんでもないサーバリソースとネットワーク帯域使ってんだなぁ、と。

おまけ。

「なにがなんでもffmpeg使いたいんだい!」っていう人のために監視につかっていたスクリプトをここに供養しておく。自分の環境に合わせてcrontabに仕掛けてください。ffmpegのプロセスが閾値を下回るとSlackのweb hookで通知してくれます。 ./monitor_ffmpeg.sh testで、テストモード(CPU監視せずに適当に閾値を下回ったことにしてSlackに通知を投げてくれる)。

#!/bin/bash

# 監視するプロセス名
PROCESS_NAME="ffmpeg"

# 監視する回数
MAX_MONITOR_COUNT=12
THRESHOLD_COUNT=3

THRESHOLD_USAGE=20

# コンテナ名
CONTAINER_NAME="<docker-container-name>"

# Slack Webhook URL
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/<slackのwebhook>"

# Slackへの通知関数
send_slack_notification() {
    message="$1"
    curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" $SLACK_WEBHOOK_URL
}

# Nginxのプロセス名
NGINX_PROCESS_NAME="nginx"

# Nginxが起動しているかチェック
is_nginx_running() {
    pgrep -f "$NGINX_PROCESS_NAME" > /dev/null
}

# プロセスのCPU利用率がしきい値を下回っているかチェック
is_usage_below_threshold() {
    CPU_USAGE=$1
    if (( $(echo "$CPU_USAGE < $THRESHOLD_USAGE" | bc -l) )); then
        return 1
    else
        return 0
    fi
}

# テストモードでの監視結果を生成
generate_test_results() {
    local results=()
    local threshold_usage_count=0
    local non_threshold_usage_count=0

    while [ ${#results[@]} -lt $MAX_MONITOR_COUNT ]; do
        if [ $threshold_usage_count -lt $THRESHOLD_COUNT ]; then
            results+=("true")
            threshold_usage_count=$((threshold_usage_count + 1))
        else
            results+=("false")
            non_threshold_usage_count=$((non_threshold_usage_count + 1))
        fi
    done

    shuf <<<"${results[*]}"
}

# Nginxが起動している場合のみ監視を行う
if is_nginx_running; then
    if [ "$1" = "test" ]; then
        echo "-------------------------"
        echo "TEST MODE: Nginx monitoring and restart script is running in test mode. No actual restart will be performed."
        echo "-------------------------"

        test_results=($(generate_test_results))

        echo "Test Results: ${test_results[@]}"

        below_threshold_count=0
        monitor_count=0
        index=0

        while true; do
            test_result=${test_results[$index]}

            if [ "$test_result" = "true" ]; then
                below_threshold_count=$((below_threshold_count + 1))
            fi

            monitor_count=$((monitor_count + 1))
            index=$((index + 1))

            if [ $monitor_count -eq $MAX_MONITOR_COUNT ]; then
                if [ $below_threshold_count -ge $THRESHOLD_COUNT ]; then
                    echo "TEST MODE: In actual mode, nginx container would have been restarted..."

                    notification_message="[TEST MODE] In actual mode, Nginx container would have been restarted due to low CPU usage."
                    send_slack_notification "$notification_message"

                    break
                fi

                monitor_count=0
                below_threshold_count=0
            fi

            sleep 5
        done

        exit 0
    else
        echo "Running in normal mode."

        below_threshold_count=0
        monitor_count=0

        while true; do
            CPU_USAGE=$(ps aux | grep -v grep | grep $PROCESS_NAME | awk '{usage+=$3} END {print usage}')
            echo "Checking process: $PROCESS_NAME with CPU usage: $CPU_USAGE"

            if ! is_usage_below_threshold $CPU_USAGE; then
                below_threshold_count=$((below_threshold_count + 1))
                echo "CPU usage is below threshold. Incremented below_threshold_count to $below_threshold_count."
            fi

            monitor_count=$((monitor_count + 1))

            if [ $below_threshold_count -ge $THRESHOLD_COUNT ]; then
                echo "Below threshold count has reached the limit. Restarting the container and sending notification..."
                docker restart $CONTAINER_NAME
                notification_message="Nginx container has been restarted due to low CPU usage."
                send_slack_notification "$notification_message"
                exit 0
            fi

            if [ $monitor_count -ge $MAX_MONITOR_COUNT ]; then
                exit 0
            fi

            sleep 5
        done
    fi
fi

実際のプロセスでスクリプトをテストしたければ下記のスクリプト用意して、上記のスクリプトの監視対象を設定するところPROCESS_NAME="<process-name>"にプロセス名を設定して、あとはプロセスを実行したりkillしたりしてやれば状態監視ができる(はず)。

#!/bin/bash
while true
do
    sleep 1
done

Discussion