블로그 목록
Media22분 읽기

RTMP 라이브 송출의 B-frame이 만든 PTS rollback — WebRTC 변환 경로의 실패 지점 추적

Origin → Wowza → Agora Media Gateway → SDRTN → Web SDK 경로에서 발생한 영상 freeze의 근본 원인은 송출단 H.264 Main profile + B-frame. Media Gateway가 '가상 호스트'로 작동하는 내부 설계(스트림 키 인코딩, 패스스루 vs 엣지 트랜스코딩), DTS/PTS 분리와 RTP 단일 타임스탬프 모델의 충돌, libwebrtc가 Constrained Baseline을 쓰는 이유, 4개 격리 테스트, Baseline / Main+bframes=0 / bitrate 하향 처방까지.

RTMPWebRTCH.264B-framePTSDTSWowzaAgora Media GatewaySDRTN트러블슈팅

RTMP 방송사에서 들어온 라이브 스트림을 Wowza로 받아 Agora Media Gateway로 중계하고, 최종적으로 Web SDK에서 시청하는 파이프라인을 운영하던 중 영상이 주기적으로 freeze되고, 수신 클라이언트 로그에 timestamp rollback 이벤트가 찍히는 증상이 관측되었습니다. 지연 영역은 아닌데 재생 자체가 불안정한 케이스였고, 원인은 송출단 H.264 인코더의 Main profile + B-frame 설정이었습니다.

이 글은 그 파이프라인의 아키텍처, 증상이 발생한 지점, 4가지 재현/격리 테스트, 그리고 왜 WebRTC 계열 시스템이 B-frame을 실질적으로 쓰지 않는가에 대한 내부 원리를 정리합니다.


1. 파이프라인 아키텍처

원본 방송사는 RTMP로 송출하고, 중간에 자체 Wowza 인스턴스가 이를 수신한 뒤 Agora Media Gateway로 릴레이합니다. Media Gateway는 이를 WebRTC(SRTP over UDP, SDRTN) 흐름으로 변환해 Web SDK 시청자에게 전달합니다.

┌──────────────────┐
│ Origin Broadcaster│
│  (H.264 인코더)   │
│  profile: main   │
│  bframes: 3      │
│  b_pyramid: normal│
│  bitrate: 5 Mbps │
└─────────┬────────┘
          │ RTMP (TCP :1935)
          ▼
┌──────────────────┐
│  Wowza Streaming │
│  Engine (re-stream)│
│  └ Incoming App  │
│  └ Stream Target │
│  ffmpeg -c copy  │   ← 재인코딩 없음 (passthrough)
└─────────┬────────┘
          │ RTMP (TCP :1935)
          ▼
┌──────────────────────────┐
│ Agora Media Gateway       │
│  RTMP ingress             │
│  → H.264/AAC 분해          │
│  → SRTP/RTP 재패킷화        │
│  → DTS/PTS 검사            │   ← 여기서 rollback 감지
│  rtls-ingress-prod-ap      │
└──────────┬────────────────┘
           │ SRTP (UDP, SDRTN)
           ▼
┌──────────────────┐
│ Agora SDRTN      │
│ (WebRTC 백본)    │
└─────────┬────────┘
          │ WebRTC (SRTP)
          ▼
┌──────────────────┐
│ Browser Viewer   │
│ Web SDK           │
│  mode: live       │
│  role: audience   │
│  codec: vp8/h264  │
└──────────────────┘

핵심 관찰: Wowza는 -c copy로 passthrough하기 때문에 Origin의 코덱 설정이 그대로 Media Gateway까지 전파됩니다. B-frame이 포함된 비트스트림이 변형 없이 WebRTC 변환 단계까지 도달한다는 뜻입니다.


1-2. Media Gateway 내부 설계 — 가상 호스트, 스트림 키, 패스스루

Agora 설계의 핵심 포인트. Media Gateway가 RTMP/SRT 스트림을 받아서 Agora SDRTN 채널에 가상의 호스트처럼 publish합니다. 수신 후 기본적으로 트랜스코딩 없이 바로 채널에 발행.

즉, Media Gateway가 채널에 접속해서 "내가 이 채널의 호스트야" 하고 스트림을 넣어주는 것. 그러면 그 채널의 모든 시청자(Web/iOS/Android SDK)는 일반 Agora 채널의 호스트 영상을 받는 것처럼 스트림을 수신합니다.

OBS / FFmpeg H.264 + AAC RTMP 송출 RTMP Agora Media Gateway 엣지 인제스트 서버 RTMP/SRT 수신 + 인증 SDRTN 진입 + 호스트 발행 선택: 엣지 트랜스코딩 SDRTN Agora 채널 라이브 프로파일 / 호스트 역할로 스트림 발행됨 채널명은 스트림 키 생성 시 지정 Agora SDRTN (글로벌 실시간 네트워크) 호스트 스트림을 전세계 엣지로 배포 / 시청자 위치 기반 최적 경로 선택 시청자 (웹) Agora Web SDK 시청자 (iOS) Agora iOS SDK 시청자 (Android) Agora Android SDK

스트림 키의 정체

단순한 인증 토큰이 아닙니다. 스트림 키 안에 어떤 채널로 보낼지 + UID가 인코딩되어 있어서, Media Gateway가 스트림 키만 보고 "이 RTMP 스트림은 channel-xyz에 UID 12345로 발행해야겠다"를 알 수 있어요.

스트림 키 = { 채널명, UID, 토큰 서명 }
         └─ Media Gateway가 디코딩해서 SDRTN publish에 사용

트랜스코딩 여부 — 매우 중요

기본값은 트랜스코딩 없음 (pass-through). RTMP로 받은 H.264를 거의 그대로 SDRTN에 발행합니다.

패스스루 장점

  • CPU 부하 없음 (서버 비용 ↓)
  • 지연 최소
  • 화질 손실 없음

패스스루의 제약 — B-frame 이슈와 직결

OBS가 기본으로 H.264 Main / High Profile + B-frame으로 송출하면:

  • Agora SDRTN은 일단 받아서 배포는 함
  • 근데 수신측 SDK (특히 웹 브라우저 WebRTC)에서 B-frame 디코딩 실패 가능
  • 뒤에서 설명할 "Main + bframes=0" 처방이 여기서 작동합니다 → 자세한 이유는 §7 해결 · Profile·인코더 옵션 가이드 참조

트랜스코딩이 필요한 경우

stream configuration template API로 설정합니다 (엣지에서 재인코딩):

  • 해상도 변환 (예: 4K 입력을 720p로 축소)
  • 비트레이트 조정
  • 오디오 코덱 변환
  • 워터마크 추가

단점: CPU 비용, 추가 지연 (~50–200ms), 인코딩 품질 손실. 판단 기준: 송출단을 완전히 통제할 수 있다면 패스스루 + 송출단 설정 조정 (x264 실전 옵션은 /frank/blog/30 참조)이 거의 항상 낫습니다. 송출단이 타사 시스템이고 Baseline/Main+bframes=0을 강제할 수 없을 때만 엣지 트랜스코딩 고려.


2. 증상 요약

  • 평균 대역폭 정상 범위 (5 Mbps 수준)
  • 오디오는 비교적 안정적
  • 영상에서 주기적 freeze → 이후 동기화되며 복구 패턴 반복
  • Media Gateway 내부 로그에 timestamp rollback, PTS < prev_PTS 유사 이벤트 확인

이 패턴은 "연결이 안 된다" 또는 "전체 지연이 크다"와는 결이 다릅니다. 개별 프레임의 타임스탬프 일관성이 깨지는 문제입니다.


3. DTS vs PTS — 왜 B-frame이 순서를 뒤집는가

H.264 비트스트림은 각 프레임마다 두 개의 타임스탬프를 가집니다.

용어풀네임의미
DTSDecoding Timestamp디코더가 이 프레임을 디코딩해야 하는 시점
PTSPresentation Timestamp이 프레임이 화면에 표시되어야 하는 시점

B-frame이 없을 때(Baseline profile 또는 bframes=0):

프레임:    I    P    P    P    P
DTS:       0    1    2    3    4
PTS:       0    1    2    3    4
→ DTS == PTS, 완전한 monotonic 증가

B-frame이 들어가면 디코더는 미래 프레임을 먼저 받아야 현재 B-frame을 복원할 수 있습니다.

표시 순서:  I    B    B    P
PTS:        0    1    2    3
         
디코딩 순서:I    P    B    B          ← P가 먼저 와야 함
DTS:        0    1    2    3
PTS:        0    3    1    2          ← PTS 비단조(non-monotonic)
                 ▲
                 "PTS가 이전보다 작다"

이 구조 자체는 MP4/MPEG-TS처럼 컨테이너가 DTS와 PTS를 별도로 전달하는 파일 포맷에서는 정상입니다. 디코더가 DTS 순서로 받되 PTS 순서로 출력하면 됩니다.

문제는 RTP에는 DTS가 없다는 것입니다.


4. RTP 타임스탬프 모델과 B-frame의 충돌

RTP 헤더에는 32비트 timestamp 하나만 있습니다 (RFC 3550).

RTP Header (12 bytes):
┌─────────────────────────────────────────────────────────┐
│V│P│X│CC│M│   PT   │     Sequence Number               │
├─────────────────────────────────────────────────────────┤
│                    Timestamp (32-bit)                   │  ← 하나뿐
├─────────────────────────────────────────────────────────┤
│                        SSRC                              │
└─────────────────────────────────────────────────────────┘

이 timestamp가 가리키는 것은 RTP 사양상 "샘플링 순간" (= presentation time) 입니다. 즉 PTS에 대응합니다 (비디오는 90 kHz clock).

H.264 over RTP 스펙(RFC 6184)은 NAL unit packetization은 정의하지만, DTS를 추가로 전달하는 표준 메커니즘이 없습니다. Media Gateway가 RTMP의 FLV 컨테이너(PTS + DTS 모두 보유)를 RTP로 재패킷화할 때 옵션은 둘 중 하나입니다:

  1. RTP timestamp에 PTS를 넣는다 → 프레임 간 timestamp가 뒤로 갔다 앞으로 갔다 하게 됨
  2. RTP timestamp에 DTS를 넣는다 → 사양 위반 (PTS여야 함), 그리고 수신측이 어떻게 해석할지 예측 불가

Agora를 포함한 대부분의 RTMP→WebRTC gateway는 (1)에 가까운 선택을 합니다. 그리고 수신측에서 jitter buffer 및 렌더링 로직이 RTP timestamp가 monotonic하게 증가한다는 전제로 설계되어 있기 때문에, PTS가 역전되는 순간 다음 중 하나가 발생합니다:

  • 이상치로 판정하고 프레임 drop
  • jitter buffer가 reset되며 순간 freeze
  • Rollback 이벤트 로그 + 복구 시도

위 증상과 정확히 일치합니다.


5. 왜 WebRTC 시스템은 실질적으로 B-frame을 안 쓰는가

B-frame은 저장형 미디어(VOD, 방송, 디스크 저장)에서는 같은 품질 대비 30~50% 대역폭을 절감하는 강력한 기법입니다. 그런데 WebRTC 영역에서는 사실상 쓰이지 않습니다. 이유는 단일하지 않고 여러 제약이 중첩됩니다.

5-1. Look-ahead 버퍼 = 내재적 지연

B-frame을 디코딩하려면 참조할 P-frame이 먼저 도착해서 디코딩되어 있어야 합니다.

시간축(송출):    t0 [I] → t1 [B] → t2 [B] → t3 [P]
                              ↓
시간축(수신):    t0 [I] → t3 [P] → t1 [B] → t2 [B]
                              ↓
화면 표시:       t0 → (t3 대기) → t1 → t2 → t3
                      ────────
                      look-ahead 지연 발생

실시간 통신의 목표 지연은 end-to-end 수백 ms 이하입니다. B-frame 하나당 수십 ms씩의 look-ahead가 추가되면 이 예산을 바로 초과합니다.

5-2. 패킷 유실에 대한 reference chain 확대

P-frame만 있을 때:

I ← P ← P ← P ← P
    ↑
    이 P가 유실되면 다음 P들이 망가지지만, 구조는 단방향

B-frame을 섞으면 참조 관계가 양방향이 됩니다 (B-frame은 이전 I/P와 이후 I/P를 모두 참조).

I ← P ← P ← P
    ↑   ↑
    B   B
    ↑   ↑
참조 체인이 방사형으로 확장

하나의 패킷이 유실되면 영향 받는 프레임 수가 훨씬 많아지고, PLI(Picture Loss Indication)로 I-frame 재요청을 해야 복구됩니다. WebRTC 환경(UDP 기반, 유실률 1~5%가 정상 범위)에서 이 페널티는 치명적입니다.

5-3. RTP 타임스탬프 모델과의 불일치

앞서 4절에서 정리한 대로, RTP는 PTS 단일 타임스탬프 모델입니다. B-frame이 들어오는 순간 타임스탬프 monotonicity가 깨지고, 수신측의 jitter buffer, A/V 동기, NACK/PLI 로직 전반이 예외 경로로 진입합니다.

이론적으로는 RTP extension header에 DTS를 추가 전달하는 방식이 가능하지만(일부 MPEG-TS over RTP 변종이 시도), WebRTC 스택은 이 확장을 구현하지 않습니다. Chrome/libwebrtc의 H.264 디코더 경로는 입력 NAL unit이 디코딩 순서 = 표시 순서라는 전제로 jitter buffer를 돌립니다.

5-4. libwebrtc의 codec profile 정책

오픈 libwebrtc(Chrome/Electron/대부분의 SDK 기반)는 H.264 profile을 SDP에서 profile-level-id로 협상합니다. 기본값은 Constrained Baseline (0x42E0XX) 또는 Constrained High 수준입니다. Constrained Baseline은 사양상 B-frame 사용 자체를 금지합니다.

SDP fmtp 예시:
a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1
                           └───┬───┘
                             42 = baseline
                             e0 = 제약 플래그 (constrained)
                             1f = level 3.1

송출측에서 Main/High profile로 보내더라도, 수신측 디코더가 Constrained 프로파일로 협상했다면 B-frame을 만나는 순간 파싱 예외 또는 드롭이 발생할 수 있습니다.

5-5. 하드웨어 디코더 가정

모바일/저전력 기기의 하드웨어 H.264 디코더는 Baseline에 최적화되어 있습니다. B-frame 지원이 있더라도 호출 경로가 상대적으로 덜 검증되었고, 일부 안드로이드 칩셋/iOS 구버전에서는 B-frame 포함 스트림에서 재생 오류를 내는 사례가 보고되어 있습니다. WebRTC SDK들이 송신측 인코더 프리셋을 Baseline 근처로 강제하는 역사적 배경입니다.

5-6. 요약

요인임팩트
Look-ahead 지연실시간 지연 예산 초과
참조 체인 확대유실 시 복구 비용 폭증
RTP 타임스탬프 단일성PTS 역전 → jitter buffer/렌더러 오동작
libwebrtc profile 기본값Constrained Baseline 협상 시 B-frame 금지
하드웨어 디코더 호환성모바일 케이스에서 드롭 위험

결론적으로 B-frame은 RTMP/CDN/VOD 세계에서는 상식적이지만, WebRTC/SDRTN 세계에서는 방해 요소입니다. 두 세계를 이어붙이는 Media Gateway가 passthrough로 동작할 때 이 마찰이 증상으로 드러납니다.


6. 재현과 격리: 4가지 테스트

가설을 "Profile/B-frame 설정이 root cause"로 두고 4개 시나리오로 격리했습니다.

Test 1 — OBS 직접 송출

OBS (H.264)
  └─ Profile: main, bframes=3 (재현 조건)
  └─ Profile: baseline, 또는 main+bframes=0 (정상 조건)
     ↓ RTMP
Wowza Incoming
     ↓ Stream Target (RTMP out, -c copy)
Agora Media Gateway
     ↓ SDRTN (SRTP)
Web SDK Viewer

결과:

  • Main + B-frame ON → 증상 재현
  • Baseline → 정상
  • Main + bframes=0 → 정상

이 시점에서 원인 후보가 네트워크/Wowza/Agora 어디도 아닌 송출단 인코더 설정이라는 것이 명확해졌습니다.

Test 2 — Multi-streaming (Agora + YouTube 동시)

Wowza Stream Target을 2개로 두고 Agora와 YouTube로 동시에 fan-out 했습니다.

OBS ─┬─→ Wowza ─┬─→ Agora Media Gateway (정상)
     │          └─→ YouTube RTMP (⚠ 끊김)

YouTube 쪽이 끊기는 건 업로드 대역폭 병목의 영향으로 보입니다. 이 테스트는 핵심 가설과 독립적이며, 동시 송출 시 업로드 대역폭 여유가 증상에 섞여 들어오는가를 확인하려는 목적이었습니다. Agora 쪽은 여전히 정상이었기 때문에 B-frame 가설과 상호작용하지 않는다는 것만 확인했습니다.

Test 3 — ffmpeg(MP4) 기반 RTMP relay

ffmpeg -re -i sample.mp4 -c copy -f flv rtmp://localhost:1935/live/myStream
   ↓
Wowza Incoming → Stream Target → Agora → Web SDK

MP4 소스에는 B-frame이 없거나 있더라도 PTS/DTS가 컨테이너에 명시적으로 저장되어 있고, ffmpeg가 RTMP로 내보낼 때 일반적으로 재-interleave됩니다. 증상 재현되지 않음.

Test 4 — Relay 레이어 추가

ffmpeg(MP4) → Wowza incoming (source1)
           → ffmpeg -c copy passthrough
           → Wowza incoming (source2)
           → Stream Target → Agora → Web SDK

중간 릴레이 홉을 한 단계 더 추가해도 MP4 소스 기반으로는 재현되지 않았습니다. 즉 Wowza나 릴레이가 문제를 유발하는 것이 아니라, 원본 비트스트림의 코덱 프로파일이 문제 본질이라는 것을 확인합니다.

테스트 매트릭스

#시나리오증상
1OBS Main + B-frame ON❌ 재현
1'OBS Baseline 또는 Main + bframes=0✅ 정상
2Multi-streaming (Agora + YouTube)Agora 정상 / YouTube 대역폭 영향
3ffmpeg MP4 → Wowza → Agora✅ 정상
4ffmpeg MP4 → Wowza → ffmpeg relay → Wowza → Agora✅ 정상

7. 해결: 송출단 조정 2가지 옵션

Option A — Baseline profile

가장 안전한 선택. H.264 Baseline은 B-frame을 정의 자체에서 금지하므로 송출 경로 어디서도 문제가 발생하지 않습니다.

ffmpeg -i input.mp4 \
  -c:v libx264 \
  -profile:v baseline \
  -level 3.1 \
  -preset veryfast \
  -tune zerolatency \
  -b:v 4M \
  -maxrate 4M -bufsize 8M \
  -g 60 -keyint_min 60 \
  -c:a aac -b:a 128k -ar 48000 \
  -f flv rtmp://wowza-host:1935/live/myStream

단, Baseline은 대역폭 효율이 떨어집니다. 같은 품질을 내려면 약 10~20% 더 높은 bitrate가 필요합니다.

Option B — Main/High + bframes=0

압축 효율(CABAC, 8x8 transform 등)은 유지하되 B-frame만 제거합니다.

ffmpeg -i input.mp4 \
  -c:v libx264 \
  -profile:v main \
  -x264-params "bframes=0:b-pyramid=none:scenecut=0" \
  -preset veryfast \
  -tune zerolatency \
  -b:v 4M \
  -maxrate 4M -bufsize 8M \
  -g 60 -keyint_min 60 \
  -c:a aac -b:a 128k -ar 48000 \
  -f flv rtmp://wowza-host:1935/live/myStream

OBS에서는 Advanced → Encoder → x264 → Custom encoder settingsbframes=0 지정하면 동일한 효과입니다.


8. 송출측 설정 검증 — ffprobe

수신측에서 원본 비트스트림의 프로파일/B-frame 사용 여부를 확인하려면:

ffprobe -v error \
  -select_streams v:0 \
  -show_entries stream=codec_name,profile,level,has_b_frames,avg_frame_rate,bit_rate,pix_fmt \
  -of default=nw=1 \
  rtmp://원본-호스트:1935/live/streamname

기대값(정상 구성):

codec_name=h264
profile=Constrained Baseline   또는   Main (bframes=0)
level=31
has_b_frames=0

has_b_frames=2 같은 값이 나오면 B-frame이 활성화되어 있는 상태입니다.


9. Bitrate를 함께 낮춘 이유

이번 케이스에서 5 Mbps → 4 Mbps 조정도 함께 권고했습니다. 이유는 분리된 두 가지입니다:

  1. Agora Media Gateway의 재패킷화 오버헤드: RTMP → SRTP 변환 과정에서 RTP/UDP 헤더가 붙고, FEC와 RTX를 고려하면 실제 업로드 대역폭은 송출 bitrate의 120~130%까지 오릅니다.
  2. 버스트 smoothing 여유: 5 Mbps 송출은 순간적으로 6~7 Mbps 버스트를 내는 경우가 있고, 이것이 경로 중간의 버퍼링과 맞물리면 jitter를 증폭시킵니다.

Bitrate 하향 단독으로는 B-frame 증상을 해결할 수 없습니다 (실제로 테스트에서 bitrate만 낮추면 증상 패턴이 유지됨). 해결은 코덱 프로파일, 완화는 bitrate라는 역할 분담입니다.


10. 진단 플레이북 (요약)

RTMP → WebRTC 변환 경로에서 "영상 freeze / 주기적 끊김 / timestamp rollback 로그" 조합이 보이면 아래 순서로 접근합니다.

1. 네트워크 계층 배제
   - 대역폭/RTT/유실률 확인
   - 정상 범위면 다음 단계로

2. 파이프라인 중간 홉 배제
   - Relay 레이어를 추가한 테스트와 제거한 테스트 비교
   - 증상이 동일하면 원본 문제

3. 코덱 프로파일 확인
   - ffprobe로 profile, has_b_frames 확인
   - Main/High + has_b_frames > 0 → 원인 유력

4. 재현 테스트
   - OBS/ffmpeg로 동일 profile 재현
   - profile만 바꿔 재테스트

5. 송출측 조정
   - Baseline, 또는 Main + bframes=0
   - 필요시 bitrate 하향 병행

11. 일반화 — 왜 이런 문제가 재발하는가

RTMP는 저장/방송 레거시 세계의 프로토콜이고, WebRTC는 양방향 저지연 통신 세계의 프로토콜입니다. 두 세계의 전제가 다릅니다.

전제RTMP/VOD 세계WebRTC 세계
지연수 초 허용수백 ms 목표
유실TCP로 재전송 보장UDP, 유실 흡수/복구 필요
타임스탬프PTS/DTS 분리PTS 단일 (RTP)
코덱 프로파일Main/High 기본Constrained Baseline 중심
GOP 구조긴 GOP + B-frame 허용짧은 GOP + P-frame 중심
버퍼링수 초 버퍼jitter buffer 수십 ms

Media Gateway는 이 두 세계를 연결하는 프로토콜 변환기이지만, 코덱 레벨까지 자동으로 정규화해주지는 않습니다 (자동 트랜스코딩은 비용이 크고, 지연을 추가합니다). 그래서 송출측이 WebRTC 세계의 제약을 만족하는 비트스트림을 내보내는 책임이 남습니다. 이번 케이스는 그 책임 경계를 명확히 보여주는 사례입니다.


정리

  • 증상: RTMP → Agora Media Gateway → WebRTC 경로에서 영상 freeze, timestamp rollback 로그
  • 원인: 송출단 H.264 인코더가 Main profile + B-frame으로 설정됨
  • 메커니즘: B-frame은 DTS와 PTS를 분리시키는데, RTP는 단일 타임스탬프 모델 → 수신측 jitter buffer가 PTS 역전을 이상치로 판정하며 drop/reset
  • WebRTC가 B-frame을 회피하는 구조적 이유: look-ahead 지연, 참조 체인 확대, RTP 타임스탬프 단일성, libwebrtc의 Constrained Baseline 기본값, 모바일 하드웨어 디코더 호환성
  • 해결: 송출측을 Baseline profile 또는 Main/High + bframes=0으로 조정. bitrate는 5 Mbps → 4 Mbps 하향으로 마진 확보
  • 격리 방법: 중간 홉을 교체하며 동일 증상 재현 여부로 책임 경계 검증

RTMP와 WebRTC를 이어붙이는 파이프라인을 설계·운영한다면, 경계면에서의 코덱 제약 차이를 기본 체크리스트로 둬야 합니다. Passthrough는 편하지만, 편한 만큼 송출측 설정이 그대로 시청자 품질에 꽂힙니다.

© 2026 Frank Kim. All rights reserved.