Skip to content

미디어 서버 분리

gamgyul163 edited this page Dec 3, 2024 · 2 revisions

미디어 서버 분리의 이유

1. 멀티비트레이트 인코딩 제약

  • SRS는 RTMP 스트림을 수신하여 기본적인 인코딩을 수행하지만, 멀티비트레이트 인코딩을 효과적으로 지원하지 못함.
  • FFmpeg를 활용한 멀티비트레이트 인코딩이 필요하며, 해당 작업은 많은 시스템 자원을 소모.

2. 확장성 확보

  • 인제스트(ingest) 서버와 인코딩(encoding) 서버를 분리하여 역할을 분담.
    • Ingest 서버: 다양한 프로토콜(예: WebRTC)을 수신하여 RTMP로 통일 후 인코딩 서버로 전달.
    • Encoding 서버: RTMP로 수신한 스트림을 멀티비트레이트로 인코딩.
  • 서버 분리로 확장성과 장비 선택의 폭 증가.

ingest 서버(SRS)

listen              1935;
max_connections     1000;
daemon              on;

http_api {
    enabled         on;
    listen          1985;
}
stats {
    network         0;
}

vhost __defaultVhost__ {
    # HTTP Hooks 설정
    http_hooks {
        enabled         on;
        # 내부 nginx로 요청을 보내 nginx에서 바디에 담긴 app 정보를 이용해 요청을 보내는 api 서버에 분기를 준다.
        on_publish      http://localhost:3000/api/validate/publish;
    }

    # RTMP 포워딩 설정
    forward {
        enabled on;
        # 원본 스트림의 app과 stream name을 유지하면서 포워딩
        destination 192.168.2.7:1935/[app]/[stream];
    }

    # WebRTC 설정
    rtc {
        enabled on;
        rtc_to_rtmp on;
    }

}

주요 설정

  • RTMP 포워딩
    • RTMP 스트림을 인코딩 서버의 RTMP 포트로 포워딩.
    • 원본 스트림의 appstream 정보를 유지.
  • WebRTC 변환
    • WebRTC로 수신한 스트림을 RTMP로 변환하여 포워딩.
  • HTTP Hooks
    • on_publish 훅을 통해 스트림이 시작되었는지 API 서버에 검증 요청.
    • Nginx를 통해 요청 라우팅.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {
    include       mime.types;
    default_type  application/octet-stream;   lua_shared_dict app_cache 10m;

    lua_need_request_body on;

    access_log /var/log/nginx/proxy_access.log;
    error_log /var/log/nginx/proxy_error.log;

    server {
        listen 3000;

        location /api/validate/publish {
            # Lua를 이용해 본문을 읽고 app과 stream 값을 기반으로 라우팅
            content_by_lua_block {
                local cjson = require("cjson")
                local http = require("resty.http")

                -- 요청 본문 읽기
                ngx.req.read_body()
                local body = ngx.req.get_body_data()

                -- JSON 본문 파싱
                local success, data = pcall(cjson.decode, body)
                local app = data.app
                local stream = data.stream

                -- app 값에 따라 대상 서버 결정
                local upstream
                if app == "live" then
                    upstream = "http://192.168.1.9:3000/lives/onair"
                elseif app == "dev" then                   upstream = "http://192.168.1.7:3000/lives/onair"
                else
                    ngx.status = ngx.HTTP_BAD_REQUEST
                    ngx.say("Invalid app")
                    return
                end

                -- stream 값이 있는지 확인
                if not stream then
                    ngx.status = ngx.HTTP_BAD_REQUEST
                    ngx.say("Stream parameter is required")
                    return
                end

                -- 동적 엔드포인트 설정
                local target_url = upstream .. "/" .. stream

                ngx.log(ngx.ERR, "Target URL :",target_url)

                local httpc = http.new()

                local res,err = httpc:request_uri(target_url, {
                method = "POST",
                body = body,
                headers = {
                ["Content-Type"] = "application/json",}})

            ngx.status = res.status

            ngx.log(ngx.ERR, "body", res.body)
            ngx.say(res.body)
        }
    }
}
}

동적 API 라우팅을 위한 Nginx Lua 활용

SRS에서 API 요청을 동적으로 분기하기 어려운 한계를 해결하기 위해 Nginx와 Lua를 사용.

  • 본문(JSON) 분석 및 라우팅app 값을 기준으로 API 서버의 엔드포인트 결정.

Encoding 서버

Nginx RTMP와 FFmpeg 조합

  • RTMP 수신 및 인코딩
    • Nginx RTMP 모듈로 RTMP 스트림 수신.
    • FFmpeg로 멀티비트레이트 인코딩 수행.
  • HLS 파일 저장
    • 동적으로 생성된 경로에 인코딩된 HLS 파일 저장.
    • Object Storage 마운트 디렉터리에 저장.
user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

error_log /var/log/nginx/rtmp_error.log;

events {
        worker_connections 1024;
}

rtmp {
        server {
                listen 1935;

                application live {
                        live on;
                        exec_publish /lico/script/stream_process.sh $app $name;

                        exec_publish_done /lico/script/cleanup.sh $app $name;

                        idle_streams off;
                        access_log /var/log/nginx/rtmp_access.log;
                        }
                application dev {
                        live on;
                        exec_publish /lico/script/stream_process.sh $app $name;

                        exec_publish_done /lico/script/cleanup.sh $app $name;

                        idle_streams off;
                        access_log /var/log/nginx/rtmp_access.log;
                        }
        }
}
#!/bin/bash

#LOG_FILE="/lico/script/logfile.log"
#exec > >(tee -a "$LOG_FILE") 2>&1

APP_NAME=$1
STREAM_KEY=$2

echo "Starting FFmpeg script for APP_NAME: $APP_NAME, STREAM_KEY: $STREAM_KEY"

# API 요청으로 채널 아이디 획득
if [[ "$APP_NAME" == "live" ]]; then
    CHANNEL_ID=$(curl -s http://192.168.1.9:3000/lives/channel-id/$STREAM_KEY | jq -r '.channelId')
elif [[ "$APP_NAME" == "dev" ]]; then
    CHANNEL_ID=$(curl -s http://192.168.1.7:3000/lives/channel-id/$STREAM_KEY | jq -r '.channelId')
else
    echo "Error: Unsupported APP_NAME. Exiting."
    exit 1
fi

if [[ -z "$CHANNEL_ID" ]]; then
    echo "Error: CHANNEL_ID is empty. Exiting."
    exit 1
fi

# ffmpeg으로 인코딩
ffmpeg -i "rtmp://192.168.1.6:1935/$APP_NAME/$STREAM_KEY" -y\
        -rw_timeout 1000000 \
    -map 0:v -map 0:a -c:a aac -b:a 192k -c:v libx264 -b:v:3 5000k -s:v:3 1920x1080 -preset superfast -profile:v baseline \
    -map 0:v -map 0:a -c:a aac -b:a 128k -c:v libx264 -b:v:2 3000k -s:v:2 1280x720 -preset superfast -profile:v baseline \
    -map 0:v -map 0:a -c:a aac -b:a 128k -c:v libx264 -b:v:1 1500k -s:v:1 854x480 -preset superfast -profile:v baseline \
    -map 0:v -map 0:a -c:a aac -b:a 128k -c:v libx264 -b:v:0 800k -s:v:0 640x360 -preset superfast -profile:v baseline \
    -hls_time 2 -hls_flags delete_segments -hls_list_size 5 \
    -hls_segment_filename "/lico/storage/$APP_NAME/$CHANNEL_ID/%v/%03d.ts" \ # APP_NAME(환경), CHANNEL_ID(채널 아이디)를 이용하여 오브젝트 스토리지 저장 경로를 동적으로 생성
    -master_pl_name "index.m3u8" \
    -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 v:3,a:3" \
    -f hls "/lico/storage/$APP_NAME/$CHANNEL_ID/%v/index.m3u8" \
    -map 0:v -vf "fps=1/10" -update 1 -s 426x240 \
    "/lico/storage/$APP_NAME/$CHANNEL_ID/thumbnail.png"
👋 소개
📖 회의록
🗓️ 개발일지
🗃 설계 문서
🕵️‍♂️ 회고록
💪 멘토링 일지
🎳 트러블 슈팅
💽 발표자료
Clone this wiki locally