-
Notifications
You must be signed in to change notification settings - Fork 0
미디어 서버 분리
gamgyul163 edited this page Dec 3, 2024
·
2 revisions
- SRS는 RTMP 스트림을 수신하여 기본적인 인코딩을 수행하지만, 멀티비트레이트 인코딩을 효과적으로 지원하지 못함.
- FFmpeg를 활용한 멀티비트레이트 인코딩이 필요하며, 해당 작업은 많은 시스템 자원을 소모.
- 인제스트(ingest) 서버와 인코딩(encoding) 서버를 분리하여 역할을 분담.
- Ingest 서버: 다양한 프로토콜(예: WebRTC)을 수신하여 RTMP로 통일 후 인코딩 서버로 전달.
- Encoding 서버: RTMP로 수신한 스트림을 멀티비트레이트로 인코딩.
- 서버 분리로 확장성과 장비 선택의 폭 증가.
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 포트로 포워딩.
- 원본 스트림의
app
및stream
정보를 유지.
-
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)
}
}
}
}
SRS에서 API 요청을 동적으로 분기하기 어려운 한계를 해결하기 위해 Nginx와 Lua를 사용.
-
본문(JSON) 분석 및 라우팅
app
값을 기준으로 API 서버의 엔드포인트 결정.
-
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"