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

이 글의 핵심

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

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

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


스트리밍 프로토콜 기초

송출은 RTMP·SRT·RTSP로 미디어 서버에 올리고, 시청은 HLS·DASH와 CDN 또는 WebRTC로 나뉘는 구조

그림: 송출(인제스트)과 배포(시청) 구간은 요구가 달라 프로토콜이 나뉜다. 송출은 RTMP·SRT·RTSP, 시청은 HTTP 세그먼트(HLS·DASH)와 CDN, 지연이 중요하면 WebRTC를 쓴다.

스트리밍과 다운로드의 차이

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

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

스트리밍의 2가지 방식

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

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

프로토콜 선택 기준

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

RTMP와 RTSP는 무엇이 다른가

두 이름이 비슷해서 같은 계열로 오해하기 쉽지만, 만들어진 목적이 다릅니다.

항목RTMPRTSP
역할영상·음성 데이터를 한 연결에 실어 보내는 전송 프로토콜재생·정지·탐색을 지시하는 제어 프로토콜 (데이터는 RTP가 따로 운반)
기본 포트TCP 1935 (암호화한 RTMPS는 443 사용이 일반적)TCP 554 (RTP 데이터는 별도 UDP 포트 또는 같은 TCP 연결에 끼워 전송)
주 사용처OBS 같은 인코더에서 방송 서버로 올리는 송출IP 카메라, CCTV, 로봇·산업용 영상
지원 코덱전통적으로 H.264·AAC. Enhanced RTMP로 HEVC·AV1 확장H.264·H.265·MJPEG 등 RTP로 실을 수 있는 코덱 대부분
브라우저 재생불가 (Flash 종료 후 플레이어 없음)불가

RTSP로 받은 카메라 영상을 웹에서 보려면 중간에 서버가 필요합니다. FFmpeg나 미디어 서버가 RTSP를 받아 HLS나 WebRTC로 바꿔 내보내는 구성이 일반적입니다. RTSP 영상이 끊기거나 화면이 깨질 때는 UDP 패킷 손실이 원인인 경우가 많아서, 먼저 TCP로 받아 보면 원인을 가를 수 있습니다.

# UDP 기본값 대신 RTP를 RTSP의 TCP 연결에 끼워서 받기
ffmpeg -rtsp_transport tcp -i rtsp://CAMERA_IP:554/stream1 -c copy out.mp4

유튜브 같은 서비스는 어떤 프로토콜을 쓰나

방송하는 쪽과 시청하는 쪽에서 쓰는 프로토콜이 다릅니다. 유튜브 라이브에 OBS로 송출할 때는 스트림 키와 함께 RTMP(또는 RTMPS) 주소를 입력합니다. 시청자는 서버가 이 입력을 여러 화질로 변환해 HTTP로 내려주는 방식, 즉 HLS나 DASH 계열의 적응형 스트리밍으로 봅니다. 그래서 “유튜브는 HLS인가 RTMP인가”라는 질문의 답은 구간마다 다르다는 것입니다. 송출은 RTMP, 시청은 HTTP 기반 세그먼트입니다.

RTMP 송출을 그대로 시청자에게 보내지 않는 이유는 두 가지입니다. 브라우저가 RTMP를 재생하지 못하고, 연결마다 서버가 상태를 들고 있어야 해서 CDN으로 시청자 수만큼 확장하기 어렵기 때문입니다. HLS와 DASH는 일반 파일 요청이라 CDN 캐시를 그대로 활용할 수 있습니다. 다만 세그먼트 단위로 모아서 내보내므로 지연이 수 초에서 십수 초 생깁니다. 이 지연을 줄이는 방법은 아래 CMAF와 LL-HLS 절에서 다룹니다.


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: IP 카메라에서 쓰는 제어 프로토콜

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

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

RTSP 구조

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

RTSP URL 형식

rtsp://username:password@192.168.1.100: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 -map a:0 -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:1 v:2,a:2" \
  stream_%v/playlist.m3u8

같은 오디오 스트림 하나(a:0)를 세 변종이 함께 가리키도록 -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0"로 쓰면 ffmpeg 9.0.2는 Same elementary stream found more than once in two different variant definitions 오류로 멈춥니다. 변종마다 오디오 스트림이 하나씩 필요하므로 -map a:0을 변종 수만큼 반복해 a:0, a:1, a:2로 나누어 연결해야 합니다(위 명령, 같은 환경에서 master.m3u8과 stream_0~2가 만들어지는 것을 확인했습니다). 오디오를 변종 사이에 공유하려면 별도 오디오 그룹(agroup)을 쓰는 방식을 씁니다.

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초)
  • 세그먼트 파일 많음

위 실험의 값으로 보면 “세그먼트 파일이 많다”는 단점이 구체적으로 이렇습니다. 24초 영상이 2초 세그먼트로 12개 파일, 5초 세그먼트로 5개 파일이 되었고, 1시간 영상이면 2초 세그먼트 기준 1,800개입니다. 세그먼트가 짧을수록 지연은 줄지만 파일 수와 요청 수가 늘어납니다.


DASH (Dynamic Adaptive Streaming over HTTP)

MPEG-DASH: XML 매니페스트를 쓰는 HLS의 대안

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. 다중 화질 인코딩 (30fps 기준 키프레임을 2초마다 고정해 화질 간 세그먼트 경계를 맞춤)
ffmpeg -i input.mp4 \
  -an -c:v libx264 -b:v 5M -s 1920x1080 -profile:v high -level 4.1 -g 60 -keyint_min 60 -sc_threshold 0 1080p.mp4 \
  -an -c:v libx264 -b:v 2.5M -s 1280x720 -profile:v high -level 4.0 -g 60 -keyint_min 60 -sc_threshold 0 720p.mp4 \
  -an -c:v libx264 -b:v 1M -s 854x480 -profile:v main -level 3.1 -g 60 -keyint_min 60 -sc_threshold 0 480p.mp4 \
  -vn -c:a aac -b:a 128k audio.mp4
# 2. DASH 패키징 (MP4Box)
MP4Box -dash 6000 -frag 6000 -rap \
       -segment-name 'segment_$RepresentationID$_' \
       -out manifest.mpd \
       1080p.mp4 720p.mp4 480p.mp4 audio.mp4

FFmpeg는 출력 파일마다 앞의 옵션을 따로 적용하므로, -an 없이 쓰면 화질별 파일마다 오디오가 한 벌씩 인코딩되어 MPD에 같은 오디오 트랙이 세 개 생깁니다. 또 -g와 -sc_threshold 0이 없으면 x264가 장면 전환마다 키프레임을 넣어 화질별 키프레임 위치가 달라지고, MP4Box가 -rap(세그먼트를 키프레임에서 시작)으로 자를 때 세그먼트 길이가 화질마다 어긋나 ABR 전환이 끊깁니다.

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: HLS와 DASH가 공유하는 세그먼트 포맷

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

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

같은 합성 영상을 FFmpeg의 -f dash -hls_playlist 1로 한 번 인코딩해 CMAF 구조를 확인했습니다. 출력 폴더에는 초기화 세그먼트 init.m4s 하나, 미디어 세그먼트 chunk_NNN.m4s 12개(2초 단위), DASH 매니페스트 manifest.mpd, HLS 매니페스트 master.m3u8과 media_0.m3u8이 들어 있었습니다. MPD의 SegmentTemplate은 initialization="init.m4s", media="chunk_$Number%03d$.m4s"를 가리키고, HLS 미디어 플레이리스트는 #EXT-X-MAP:URI="init.m4s"와 chunk_001.m4s를 가리켰습니다. 두 매니페스트가 같은 세그먼트 파일을 참조하므로 세그먼트를 한 벌만 저장하면 HLS와 DASH를 모두 서비스할 수 있다는 설명이 확인됩니다. HLS 플레이리스트는 #EXT-X-VERSION:6이었고 .ts 기반 HLS(#EXT-X-VERSION:3)와는 세그먼트 컨테이너가 다릅니다. 저지연 HLS(LL-HLS)와 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:

tiangolo/nginx-rtmp 이미지에는 nginx와 RTMP 모듈만 들어 있고 FFmpeg가 없습니다. 아래 설정의 exec ffmpeg(화질별 트랜스코딩)는 이미지에 FFmpeg를 추가해야 동작하므로, 작은 Dockerfile로 감싸서 빌드합니다. FFmpeg가 없으면 nginx는 정상적으로 뜨고 원본 스트림도 받지만, 변환 화질만 조용히 생기지 않아 원인을 찾기 어렵습니다.

# Dockerfile
FROM tiangolo/nginx-rtmp
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
    && rm -rf /var/lib/apt/lists/*
# 실행 예제
services:
  nginx-rtmp:
    build: .
    ports:
      - "1935:1935"  # RTMP
      - "8080:80"    # HLS/DASH
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./hls:/tmp/hls
      - ./dash:/tmp/dash

nginx.conf (완전판):

worker_processes auto;
events {}  # 없으면 nginx가 'no "events" section' 오류로 시작하지 않음

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;
        }
    }
}

# 컨테이너 80번 포트(compose에서 8080으로 연결)로 HLS/DASH 파일 제공
http {
    server {
        listen 80;
        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 *;
        }
        location /dash {
            types {
                application/dash+xml mpd;
                video/mp4 mp4 m4s;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
        }
    }
}

이런 예제에서 흔히 빠지는 세 가지를 보강했습니다. 재송출 대상 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)

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

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

직접 재현했습니다. macOS(FFmpeg 9.0.2, libx264)에서 testsrc2 1280×720, 30fps, 24초 합성 영상을 -f hls -hls_time 2로 만들면서 키프레임 간격(-g)만 바꿨습니다.

키프레임 간격세그먼트 길이세그먼트 수
150프레임(5초)5.0초 네 개, 마지막 4.0초5개
60프레임(2초)모두 2.0초12개

-hls_time 2를 줬는데도 키프레임이 5초마다만 있으면 세그먼트는 5초로 잘렸습니다. 세그먼트는 키프레임에서만 자를 수 있기 때문입니다. 그래서 “세그먼트를 2초로 줄였는데 지연이 그대로”라면 인코더 설정의 키프레임 간격을 먼저 확인해야 한다는 설명이 맞았습니다.

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

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