RTMP·RTSP·HLS·DASH·CMAF·WebRTC 비교: 스트리밍 프로토콜별 지연과 용도

이 글의 핵심

수집용 RTMP·RTSP와 배포용 HLS·DASH, 저지연을 위한 CMAF와 WebRTC가 각각 어떻게 동작하는지, 지연 시간과 호환성 기준으로 어떤 조합을 고를지 정리합니다.

들어가며

영상 스트리밍 프로토콜은 영상을 네트워크로 어떻게 잘라서 보내고, 받는 쪽이 어떻게 이어서 재생할지를 정의합니다. YouTube, Netflix, Twitch 같은 서비스는 모두 여러 프로토콜을 조합해 씁니다.

실제 서비스는 한 가지 프로토콜만 쓰지 않습니다. 방송 장비에서 서버로 올리는 구간(인제스트)과 서버에서 시청자에게 내려보내는 구간(배포)의 요구가 다르기 때문입니다. 이 글은 RTMP·RTSP·SRT 같은 인제스트 프로토콜, HLS·DASH와 이를 묶는 CMAF, 서브초 지연을 위한 WebRTC의 동작 원리와 FFmpeg·nginx 구성 예제를 살펴보고, 지연 시간과 호환성 기준으로 조합을 고르는 방법과 자주 만나는 문제의 원인을 정리합니다.


스트리밍 프로토콜 기초

스트리밍이란?

스트리밍은 영상을 전부 받기 전에, 받는 동시에 재생하는 방식입니다.

다운로드:
[========================================] 100%
다운로드 완료 후 재생
스트리밍:
[=====>                                  ] 10%
다운로드 중인데 이미 재생 시작!

스트리밍의 2가지 방식

1. 라이브 스트리밍 (Live Streaming)

  • 실시간 방송 (Twitch, YouTube Live)
  • 낮은 지연시간 중요
  • 예: 스포츠 중계, 게임 방송 2. VOD (Video on Demand)
  • 녹화된 영상 (Netflix, YouTube)
  • 안정성과 품질 중요
  • 예: 영화, 드라마

프로토콜 선택 기준

기준설명
지연시간실시간성 (1초 vs 30초)
확장성동시 시청자 수 (100명 vs 100만명)
호환성브라우저/기기 지원
비용서버/CDN 비용
품질화질, 적응형 스트리밍

RTMP (Real-Time Messaging Protocol)

RTMP란?

RTMP는 Adobe(원래 Macromedia)가 Flash용으로 만든 실시간 전송 프로토콜로, 지금은 라이브 방송을 서버로 올리는 인제스트 구간의 사실상 표준입니다. 특징:

  • TCP 기반 (안정적이지만 패킷 손실 시 재전송 대기로 지연이 쌓임)
  • 인제스트 구간 지연은 1~5초 수준
  • 원래 Flash 플레이어용이었으나 Flash 종료로 브라우저 재생은 불가능해졌고, 지금은 송출(인제스트) 전용으로 쓰임
  • 2023년 이후 Enhanced RTMP 확장으로 HEVC·AV1·VP9 송출도 가능 (OBS·YouTube 등 지원)

RTMP 워크플로우

[스트리머] → RTMP → [서버] → HLS/DASH → [시청자]
   (OBS)              (Nginx)             (브라우저)
1. 스트리머: OBS로 RTMP 송출
2. 서버: RTMP 수신 → HLS/DASH 변환
3. 시청자: HLS/DASH로 시청

RTMP 구현 (Nginx)

nginx.conf:

rtmp {
    server {
        listen 1935;  # RTMP 포트
        
        application live {
            live on;  # 라이브 스트리밍 활성화
            
            # HLS 변환
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3s;  # 세그먼트 길이
            hls_playlist_length 60s;
            
            # DASH 변환
            dash on;
            dash_path /tmp/dash;
            dash_fragment 3s;
        }
    }
}
http {
    server {
        listen 8080;
        
        # HLS 제공
        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
        }
        
        # DASH 제공
        location /dash {
            types {
                application/dash+xml mpd;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
        }
    }
}

OBS 설정:

서버: rtmp://your-server.com/live
스트림 키: stream-key-123

nginx-rtmp-module은 학습·사내용으로는 여전히 간편하지만, 원본 저장소는 수년째 사실상 유지보수가 멈춰 있고 LL-HLS·SRT·WebRTC를 지원하지 않습니다. 새로 구축한다면 SRS, MediaMTX, OvenMediaEngine 같은 활발한 미디어 서버나 클라우드 라이브 서비스를 먼저 검토하는 편이 낫습니다. 또 OBS의 키프레임 간격을 “자동(0)“으로 두면 x264가 최대 250프레임(30fps 기준 약 8초)마다 키프레임을 넣어서, 위에서 설정한 hls_fragment 3s가 지켜지지 않고 세그먼트가 키프레임 단위로 길게 잘립니다. 세그먼트는 키프레임에서만 자를 수 있으므로, 키프레임 간격을 세그먼트 길이의 약수(보통 1~2초)로 명시해야 합니다.

RTMP 장단점

장점:

  • 낮은 지연시간 (1-5초)
  • 안정적인 전송 (TCP)
  • 업계 표준 (모든 인코더 지원) 단점:
  • 브라우저 직접 재생 불가 (Flash 종료)
  • 방화벽 문제 (1935 포트, RTMPS는 443 사용 가능)
  • 확장성 제한 (시청자 배포용이 아님)
  • TCP라서 해외 송출·모바일 망처럼 손실이 있는 구간에서 지연이 누적되고 끊김이 잦음

RTMP 대안 인제스트: SRT와 WHIP

RTMP의 약점이 드러나는 대표적인 상황이 원거리 송출입니다. 서울에서 미국 서버로 RTMP를 보내면 왕복 지연이 100ms를 훌쩍 넘고, 패킷 하나가 손실될 때마다 TCP가 재전송을 기다리면서 송출 비트레이트가 출렁입니다. SRT(Secure Reliable Transport)는 UDP 위에서 손실된 패킷만 선택적으로 재전송하고, 재전송을 기다릴 최대 시간(latency, 보통 RTT의 3~4배)을 명시적으로 정하는 방식이라 손실 구간에서 훨씬 안정적입니다. 방송 장비와 OBS, FFmpeg, 주요 미디어 서버가 모두 지원합니다.

# FFmpeg로 SRT 송출 (latency는 마이크로초 단위, 여기선 200ms)
ffmpeg -re -i input.mp4 -c:v libx264 -g 60 -c:a aac \
  -f mpegts "srt://ingest.example.com:9000?mode=caller&latency=200000"

WHIP(WebRTC-HTTP Ingestion Protocol)은 WebRTC 송출을 HTTP POST 한 번으로 시작하게 표준화한 규격으로, OBS 30부터 기본 지원합니다. 서브초 지연이 필요한 경매·인터랙티브 방송이라면 WHIP 송출 + WebRTC 배포 조합을, 그 외 대부분은 SRT 또는 RTMP 송출 + HLS 배포 조합을 씁니다.


RTSP (Real Time Streaming Protocol)

RTSP란?

RTSP는 IP 카메라와 CCTV에서 주로 쓰는 프로토콜입니다. RTSP 자체는 재생·정지 같은 제어 명령만 주고받고, 영상 데이터는 RTP로 따로 흐릅니다. 특징:

  • UDP/TCP 지원
  • 양방향 제어 (재생, 일시정지, 탐색)
  • RTP/RTCP로 실제 데이터 전송

RTSP 구조

RTSP (제어 채널)
  ↓
RTP (미디어 전송)
  ↓
RTCP (품질 피드백)

RTSP URL 형식

rtsp://username:[email protected]:554/stream1
rtsp://     - 프로토콜
username    - 인증 사용자명
password    - 인증 비밀번호
192.168.1.100 - IP 주소
554         - 포트 (기본값)
/stream1    - 스트림 경로

RTSP 재생 (FFmpeg)

# RTSP 스트림 재생
ffplay rtsp://192.168.1.100:554/stream1
# RTSP → RTMP 변환
ffmpeg -i rtsp://192.168.1.100:554/stream1 \
       -c copy \
       -f flv rtmp://your-server.com/live/stream
# RTSP → HLS 변환
ffmpeg -i rtsp://192.168.1.100:554/stream1 \
       -c:v libx264 -c:a aac \
       -f hls -hls_time 3 -hls_list_size 10 \
       output.m3u8

RTSP 장단점

장점:

  • 낮은 지연시간
  • 양방향 제어
  • IP 카메라 표준 단점:
  • 브라우저 지원 없음
  • 방화벽 문제 (UDP)
  • 확장성 제한

HLS (HTTP Live Streaming)

HLS란?

HLS는 Apple이 만든 HTTP 기반 스트리밍 프로토콜로, 현재 시청자 배포에 가장 널리 쓰입니다. 영상을 몇 초 단위 파일로 잘라 두고 플레이리스트로 순서를 알려 주는 단순한 구조라서, 일반 웹 서버와 CDN을 그대로 활용할 수 있습니다. 특징:

  • HTTP 기반 (CDN 활용 가능)
  • 적응형 비트레이트 (ABR)
  • 모든 Apple 기기 지원

HLS 구조

영상 → 세그먼트로 분할 → Playlist 생성
video.mp4
  ↓
segment0.ts (6초)
segment1.ts (6초)
segment2.ts (6초)
  ↓
playlist.m3u8 (세그먼트 목록)

HLS Playlist (M3U8)

Master Playlist (다중 화질):

#EXTM3U
#EXT-X-VERSION:3
# 1080p
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p.m3u8
# 720p
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
720p.m3u8
# 480p
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=854x480
480p.m3u8

Media Playlist (세그먼트 목록):

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.0,
segment0.ts
#EXTINF:6.0,
segment1.ts
#EXTINF:6.0,
segment2.ts
#EXT-X-ENDLIST

HLS 생성 (FFmpeg)

# 단일 화질 HLS
ffmpeg -i input.mp4 \
       -c:v libx264 -c:a aac \
       -f hls \
       -hls_time 6 \
       -hls_list_size 0 \
       -hls_segment_filename 'segment%03d.ts' \
       playlist.m3u8
# 다중 화질 HLS (ABR)
ffmpeg -i input.mp4 \
  -filter_complex \
  "[0:v]split=3[v1][v2][v3]; \
   [v1]scale=w=1920:h=1080[v1out]; \
   [v2]scale=w=1280:h=720[v2out]; \
   [v3]scale=w=854:h=480[v3out]" \
  -map "[v1out]" -c:v:0 libx264 -b:v:0 5M \
  -map "[v2out]" -c:v:1 libx264 -b:v:1 2.5M \
  -map "[v3out]" -c:v:2 libx264 -b:v:2 1M \
  -map a:0 -c:a aac -b:a 128k \
  -f hls -hls_time 6 \
  -hls_playlist_type vod \
  -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \
  stream_%v/playlist.m3u8

HLS 재생 (JavaScript)

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
    <video id="video" controls width="640"></video>
    
    <script>
        const video = document.getElementById('video');
        const videoSrc = 'https://example.com/master.m3u8';
        
        if (video.canPlayType('application/vnd.apple.mpegurl')) {
            // Safari (네이티브 HLS 지원)
            video.src = videoSrc;
        } else if (Hls.isSupported()) {
            // 다른 브라우저 (hls.js 사용)
            const hls = new Hls();
            hls.loadSource(videoSrc);
            hls.attachMedia(video);
            
            hls.on(Hls.Events.MANIFEST_PARSED, () => {
                video.play();
            });
        }
    </script>
</body>
</html>

HLS 장단점

장점:

  • HTTP 기반 (CDN 활용)
  • 방화벽 문제 없음
  • 적응형 비트레이트 (ABR)
  • Apple 기기 네이티브 지원
  • CDN 캐시를 통한 대규모 확장 단점:
  • 높은 지연시간 (6-30초)
  • 세그먼트 파일 많음

DASH (Dynamic Adaptive Streaming over HTTP)

DASH란?

MPEG-DASH는 MPEG에서 표준화한 HTTP 적응형 스트리밍 규격으로, HLS의 국제 표준 대안입니다. 구조는 HLS와 비슷하지만 플레이리스트 대신 XML 형식의 MPD 매니페스트를 씁니다. 특징:

  • HTTP 기반
  • 코덱 독립적 (H.264, H.265, VP9, AV1)
  • DRM 지원 우수

DASH 구조

영상 → 세그먼트 분할 → MPD 생성
video.mp4
  ↓
init.mp4 (초기화 세그먼트)
segment1.m4s
segment2.m4s
segment3.m4s
  ↓
manifest.mpd (세그먼트 목록)

DASH Manifest (MPD)

<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011">
  <Period>
    <!-- 비디오 -->
    <AdaptationSet mimeType="video/mp4" codecs="avc1.4d401f">
      <!-- 1080p -->
      <Representation id="1" bandwidth="5000000" width="1920" height="1080">
        <SegmentTemplate timescale="1000" 
                         initialization="init-$RepresentationID$.mp4"
                         media="segment-$RepresentationID$-$Number$.m4s"
                         startNumber="1">
          <SegmentTimeline>
            <S t="0" d="6000" r="9"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
      
      <!-- 720p -->
      <Representation id="2" bandwidth="2500000" width="1280" height="720">
        <!-- ... -->
      </Representation>
    </AdaptationSet>
    
    <!-- 오디오 -->
    <AdaptationSet mimeType="audio/mp4" codecs="mp4a.40.2">
      <Representation id="audio" bandwidth="128000">
        <!-- ... -->
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

DASH 생성 (FFmpeg + MP4Box)

# 1. 다중 화질 인코딩
ffmpeg -i input.mp4 \
  -c:v libx264 -b:v 5M -s 1920x1080 -profile:v high -level 4.1 1080p.mp4 \
  -c:v libx264 -b:v 2.5M -s 1280x720 -profile:v high -level 4.0 720p.mp4 \
  -c:v libx264 -b:v 1M -s 854x480 -profile:v main -level 3.1 480p.mp4
# 2. DASH 패키징 (MP4Box)
MP4Box -dash 6000 -frag 6000 -rap \
       -segment-name 'segment_$RepresentationID$_' \
       -out manifest.mpd \
       1080p.mp4 720p.mp4 480p.mp4

DASH 재생 (dash.js)

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
</head>
<body>
    <video id="video" controls width="640"></video>
    
    <script>
        const url = 'https://example.com/manifest.mpd';
        const player = dashjs.MediaPlayer().create();
        player.initialize(document.getElementById('video'), url, true);
    </script>
</body>
</html>

DASH 장단점

장점:

  • 오픈 표준 (로열티 없음)
  • 코덱 독립적
  • DRM 지원 우수
  • HTTP 기반 단점:
  • iPhone의 Safari는 오랫동안 MSE를 지원하지 않아 DASH 재생이 어려웠음 (iOS 17.1부터 ManagedMediaSource로 일부 가능)
  • 복잡한 구현

CMAF (Common Media Application Format)

CMAF란?

CMAF는 프로토콜이 아니라 세그먼트 포맷 규격입니다. fMP4 기반으로 세그먼트 형식을 통일해, 같은 세그먼트 파일을 HLS와 DASH 양쪽에서 쓸 수 있게 합니다. 특징:

  • fMP4 기반
  • HLS + DASH 동시 지원
  • 낮은 지연시간 (LL-HLS, LL-DASH)

CMAF 구조

기존:
HLS용 파일 (TS)  +  DASH용 파일 (MP4)
→ 2배 저장 공간, 2배 인코딩 시간
CMAF:
공통 파일 (fMP4)
→ HLS와 DASH 모두 사용
→ 1배 저장 공간, 1배 인코딩 시간

CMAF 생성 (FFmpeg)

# CMAF 인코딩
ffmpeg -i input.mp4 \
  -c:v libx264 -b:v 5M -s 1920x1080 -profile:v high \
  -c:a aac -b:a 128k \
  -f hls \
  -hls_segment_type fmp4 \
  -hls_fmp4_init_filename init.mp4 \
  -hls_segment_filename 'segment%03d.m4s' \
  -hls_playlist_type vod \
  playlist.m3u8

LL-HLS (Low-Latency HLS)

특징:

  • 지연시간: 대략 2~5초 (일반 HLS의 수십 초 대비 크게 단축)
  • 부분 세그먼트(Partial Segment): 세그먼트가 다 만들어지기 전에 0.2~1초짜리 조각을 먼저 공개
  • 블로킹 플레이리스트 리로드: 플레이어가 “다음 조각이 나오면 응답해 달라”고 요청을 걸어 두어 폴링 낭비를 없앰
  • 프리로드 힌트(EXT-X-PRELOAD-HINT): 다음 조각 URL을 미리 알려 줘서 요청을 먼저 보내 둠

초기 LL-HLS 초안은 HTTP/2 Server Push를 요구했지만 CDN 지원이 어려워 2020년 정식 규격에서 제거되고 프리로드 힌트로 대체됐습니다. 오래된 자료를 보고 Push 설정부터 찾는 경우가 많은데, 지금은 필요 없습니다. 대신 CDN이 블로킹 요청(_HLS_msn, _HLS_part 쿼리)을 오리진까지 그대로 전달하고 쿼리별로 캐시 키를 나누는지가 실제 관건입니다.

#EXTM3U
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:6
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.0
#EXT-X-PART-INF:PART-TARGET=0.5
#EXT-X-PART:DURATION=0.5,URI="part0.m4s"
#EXT-X-PART:DURATION=0.5,URI="part1.m4s"
#EXTINF:6.0,
segment0.m4s
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="part2.m4s"

CMAF 장단점

장점:

  • HLS + DASH 동시 지원
  • HLS용·DASH용 세그먼트를 따로 만들지 않아 저장 공간과 인코딩 부담이 줄어듦
  • 낮은 지연시간 (LL-HLS) 단점:
  • 아주 오래된 기기(iOS 10 이전, 일부 구형 스마트 TV)는 fMP4 HLS를 재생하지 못해 TS 폴백이 필요할 수 있음
  • 암호화 방식이 갈리면(cenc vs cbcs) 결국 두 벌이 필요 → 최신 기기는 cbcs 한 벌로 통일 가능
  • 저지연(LL-HLS/LL-DASH)까지 가면 CDN·플레이어 설정 난이도가 급격히 올라감

“하나의 파일로 HLS와 DASH를 모두 지원”한다는 장점은 세그먼트 파일에 대한 이야기이고, 매니페스트(.m3u8와 .mpd)는 여전히 각각 만들어야 합니다. 실제 절감 효과는 스토리지보다 CDN 캐시에서 더 크게 나타납니다. 같은 세그먼트를 HLS 시청자와 DASH 시청자가 함께 받아 가니 캐시 적중률이 올라가고 오리진 부하가 줄어듭니다.


WebRTC (Web Real-Time Communication)

WebRTC란?

WebRTC는 브라우저 간 실시간 통신을 위한 API와 프로토콜 묶음으로, 서브초 지연이 특징입니다. 버퍼를 최소한으로만 두고 늦게 도착한 패킷은 과감히 버리는 설계라서 지연이 낮은 대신, 화질이 망 상태에 더 민감하게 반응합니다. 특징:

  • UDP 기반 (SRTP)
  • 지연시간: 0.5초 미만도 가능
  • 브라우저 네이티브 지원
  • 1:1이나 소규모는 P2P, 대규모 방송은 SFU(Selective Forwarding Unit) 서버가 중계

WebRTC 구조

[발신자] ←→ Signaling Server ←→ [수신자]
    ↓                              ↓
    └──────── P2P 연결 ────────────┘
           (STUN/TURN)

WebRTC 구현 (JavaScript)

발신자:

// 1. MediaStream 획득
const stream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
});
// 2. RTCPeerConnection 생성
const pc = new RTCPeerConnection({
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
    ]
});
// 3. 스트림 추가
stream.getTracks().forEach(track => {
    pc.addTrack(track, stream);
});
// 4. Offer 생성
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 5. Offer 전송 (Signaling Server)
sendToSignalingServer({ type: 'offer', sdp: offer.sdp });

수신자:

// 1. RTCPeerConnection 생성
const pc = new RTCPeerConnection({
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
    ]
});
// 2. 트랙 수신 핸들러는 협상 전에 등록
pc.ontrack = (event) => {
    videoElement.srcObject = event.streams[0];
};
// 3. Offer 수신 (offer = { type: 'offer', sdp })
await pc.setRemoteDescription(offer);
// 4. Answer 생성
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 5. Answer 전송 (ICE 후보는 onicecandidate로 따로 교환해야 함)
sendToSignalingServer({ type: 'answer', sdp: answer.sdp });

WebRTC 장단점

장점:

  • 초저지연 (0.5-1초)
  • P2P 연결 가능 (소규모 통화에서는 서버 부하가 적음)
  • 브라우저 네이티브 지원 단점:
  • 대규모 배포 시 SFU 서버 비용 (HTTP CDN처럼 캐시가 되지 않아 시청자 수에 비례해 비용 증가)
  • 방화벽/NAT 문제 (TURN 서버 필요, 기업망에서 특히)
  • ABR이 HLS만큼 유연하지 않음 (Simulcast/SVC로 보완)

“WebRTC는 확장이 안 된다”는 말은 P2P 구조일 때 얘기입니다. SFU를 여러 단계로 두면 수만 명 규모 방송도 가능하고, 클라우드 서비스들이 WHEP(재생 측 표준)로 이런 배포를 제공합니다. 다만 시청자 한 명당 서버 연결이 유지되므로 같은 규모의 HLS보다 비용이 크게 드는 것이 현실적인 제약입니다.


프로토콜 비교

전체 비교표

프로토콜지연시간확장성브라우저 지원주요 용도
RTMP1-5초중간❌라이브 인제스트
RTSP1-3초낮음❌IP 카메라
HLS6-30초높음✅VOD, 라이브
DASH6-30초높음✅VOD, 라이브
CMAF (LL-HLS/LL-DASH)2-5초높음✅저지연 라이브
SRT설정값(보통 0.2~2초)낮음❌원거리·손실 망 인제스트
WebRTC0.5초 미만~1초중간 (SFU 필요)✅화상 통화, 인터랙티브 방송

상황별 추천

1. 라이브 스트리밍 (대규모)

RTMP (인제스트) → HLS/DASH (배포)
지연시간: 10-30초
확장성: 무제한

2. 라이브 스트리밍 (저지연)

RTMP → LL-HLS 또는 WebRTC
지연시간: 1-3초
확장성: 중간

3. VOD (주문형 비디오)

HLS (Apple) + DASH (Android)
또는 CMAF (통합)

4. 화상 통화

WebRTC
지연시간: 1초 미만

5. IP 카메라

RTSP → RTMP/HLS 변환

실전 구현

완전한 라이브 스트리밍 시스템

아키텍처:

[스트리머] → RTMP → [Nginx-RTMP] → HLS/DASH → [CDN] → [시청자]
   (OBS)              (서버)                    (CloudFlare)  (브라우저)

Docker Compose:

# 실행 예제
version: '3'
services:
  nginx-rtmp:
    image: tiangolo/nginx-rtmp
    ports:
      - "1935:1935"  # RTMP
      - "8080:80"    # HLS/DASH
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./hls:/tmp/hls
      - ./dash:/tmp/dash

nginx.conf (완전판):

rtmp {
    server {
        listen 1935;
        chunk_size 4096;
        
        application live {
            live on;
            record off;
            
            # 인증 (선택)
            on_publish http://localhost:8000/auth;
            
            # HLS
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3s;
            hls_playlist_length 60s;
            hls_continuous on;
            hls_cleanup on;
            hls_nested on;
            
            # DASH
            dash on;
            dash_path /tmp/dash;
            dash_fragment 3s;
            dash_playlist_length 60s;
            dash_cleanup on;
            dash_nested on;
            
            # 트랜스코딩 (다중 화질) → 아래 hls 애플리케이션으로 재송출
            # -g 60 -keyint_min 60 -sc_threshold 0: 모든 화질의 키프레임 위치를 2초(30fps)로 정렬
            exec ffmpeg -i rtmp://localhost/live/$name
              -c:a aac -b:a 128k -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 -b:v 5M -s 1920x1080 -f flv rtmp://localhost/hls/$name_1080p
              -c:a aac -b:a 128k -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 -b:v 2500k -s 1280x720 -f flv rtmp://localhost/hls/$name_720p
              -c:a aac -b:a 96k -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 -b:v 1M -s 854x480 -f flv rtmp://localhost/hls/$name_480p;
        }

        application hls {
            live on;
            allow publish 127.0.0.1;  # exec ffmpeg만 송출 가능
            deny publish all;
            hls on;
            hls_path /tmp/hls;
            hls_nested on;
            hls_fragment 2s;
            # 마스터 플레이리스트(/hls/$name.m3u8)에 화질별 변형 등록
            hls_variant _1080p BANDWIDTH=5300000,RESOLUTION=1920x1080;
            hls_variant _720p  BANDWIDTH=2800000,RESOLUTION=1280x720;
            hls_variant _480p  BANDWIDTH=1200000,RESOLUTION=854x480;
        }
    }
}

이런 예제에서 흔히 빠지는 세 가지를 보강했습니다. 재송출 대상 hls 애플리케이션이 정의돼 있어야 하고, 화질별로 오디오 코덱을 지정해야 하며, 모든 화질의 키프레임이 같은 시각에 있어야 합니다. 마지막이 빠지면 x264가 장면 전환마다 키프레임을 넣어 화질별 세그먼트 경계가 달라지고, 플레이어가 ABR로 화질을 바꾸는 순간 화면이 튀거나 잠깐 멈춥니다. /live에는 원본을, /hls에는 변환본을 두는 구조라 원본까지 HLS로 만들 필요가 없다면 /live의 hls on은 꺼도 됩니다.


트러블슈팅

HLS 지연시간 줄이기

문제: HLS 지연시간이 30초 이상

원인: HLS 지연은 대략 “인코더 버퍼 + 세그먼트 길이 × 플레이어가 뒤에서 대기하는 세그먼트 수(보통 3개) + CDN 전파”로 결정됩니다. 6초 세그먼트면 플레이어 대기만으로 18초가 깔고 들어갑니다.

해결:

# 세그먼트 길이 단축 (송출 측 키프레임 간격도 2초 이하여야 효과가 있음)
hls_fragment 2s;
# 플레이리스트 길이 단축
hls_playlist_length 10s;
// 플레이어가 라이브 끝에서 몇 세그먼트 뒤를 재생할지
const hls = new Hls({ liveSyncDurationCount: 2 }); // 기본 3

세그먼트를 줄이면 요청 수가 늘어 CDN 비용과 오리진 부하가 커지고, 네트워크가 흔들릴 때 버퍼 여유가 줄어 끊김이 늘어납니다. 일반 HLS로는 2초 세그먼트·6~10초 지연 정도가 현실적인 하한이고, 그보다 낮춰야 한다면 세그먼트를 계속 쪼개기보다 LL-HLS나 WebRTC로 방식을 바꾸는 편이 낫습니다. 송출 측 키프레임 간격이 4초인데 hls_fragment만 2초로 바꾸면 세그먼트는 여전히 4초로 잘린다는 점도 자주 놓칩니다.

CORS 에러

문제: 브라우저에서 HLS 재생 안 됨 해결:

location /hls {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "GET, OPTIONS";
    add_header Access-Control-Allow-Headers "Range";
}

버퍼링 문제

문제: 영상이 자주 멈춤

버퍼 크기를 키우는 것은 대부분 해결책이 아닙니다. hls.js의 기본값(maxBufferLength: 30초)은 이미 넉넉하고, 라이브에서는 버퍼를 늘릴수록 지연만 커집니다. 먼저 원인을 나눠 보세요.

  • 특정 시청자만 멈춤: ABR 사다리에 낮은 화질(480p 이하, 1Mbps 이하)이 충분히 있는지 확인합니다. 최저 화질이 너무 높으면 느린 망에서 내려갈 곳이 없습니다.
  • 모든 시청자가 동시에 멈춤: 송출 측 문제입니다. 인코더 CPU 과부하(OBS의 “인코딩 과부하” 경고), 송출 망 업로드 부족, 세그먼트가 실시간보다 늦게 생성되는지 서버 로그를 확인합니다.
  • 화질 전환 순간에만 멈춤: 화질 간 키프레임이 정렬되지 않은 경우입니다(위 nginx 예제의 -sc_threshold 0 설명 참고).
const hls = new Hls();
hls.on(Hls.Events.ERROR, (_, data) => {
  // bufferStalledError가 잦으면 망/ABR, fragLoadError면 CDN/오리진 쪽을 의심
  console.warn(data.type, data.details, data.fatal);
});

자주 묻는 질문 (FAQ)

Q. HLS 세그먼트를 2초로 줄였는데 지연이 그대로예요.

A. 송출 측 키프레임 간격이 세그먼트보다 길면 세그먼트는 키프레임 단위로만 잘립니다. OBS·인코더의 키프레임 간격을 2초로 명시하고, 플레이어의 라이브 대기 세그먼트 수(hls.js liveSyncDurationCount)도 함께 확인하세요.

Q. 해외 서버로 송출할 때 RTMP가 자주 끊깁니다.

A. TCP 기반 RTMP는 손실이 있는 장거리 구간에 약합니다. 송출 경로를 SRT로 바꾸거나, 가까운 리전의 인제스트 엔드포인트로 받은 뒤 내부망으로 전달하는 구성을 검토하세요.


같이 보면 좋은 글