블로그 목록
Media22분 읽기

RTMP-WebRTC 변환의 Timestamp Rollback 진단

RTMP ingest를 WebRTC로 변환하는 경로에서 timestamp rollback과 freeze가 나타난 사례를 진단합니다. B-frame 재정렬, PTS·DTS 변환, transcoder·gateway 처리 경계를 각각 격리하며, 특정 H.264 profile을 원인으로 단정하기 전에 bitstream과 단계별 timestamp를 비교하는 절차를 설명합니다.

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

RTMP 방송사에서 들어온 라이브 스트림을 Wowza로 받아 Agora Media Gateway로 중계하고, 최종적으로 Web SDK에서 시청하는 파이프라인을 운영하던 중 영상이 주기적으로 freeze되고, 수신 클라이언트 로그에 timestamp rollback 이벤트가 찍히는 증상이 관측되었습니다. 이 사례에서는 송출단 H.264의 B-picture 사용 여부를 바꿨을 때 증상이 함께 사라졌습니다. 다만 이는 해당 파이프라인의 격리 테스트 결과이며, RTP 표준이 B-picture를 금지한다는 뜻은 아닙니다.

이 글은 그 파이프라인의 관측 범위, 4가지 재현/격리 테스트, 그리고 저지연 WebRTC 인코더에서 B-picture를 흔히 끄는 이유를 정리합니다. 제품 내부 동작은 공개 API 계약과 사례 당시 로그에서 확인한 추정을 구분해 적습니다.


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  │
└──────────────────┘

핵심 관찰: -c copy 구간은 비디오를 재인코딩하지 않으므로 H.264 picture 구조는 유지됩니다. 다만 container timestamp와 interleaving은 muxer에서 다시 표현될 수 있으므로 -c copy만으로 timestamp 처리까지 동일하다고 단정할 수는 없습니다.


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

공개 제품 관점에서 Media Gateway는 외부 RTMP/SRT 입력을 Agora 채널에 publish하는 ingest 경계입니다. 아래의 "가상 호스트"는 데이터 흐름을 설명하기 위한 비유이며 실제 wire protocol이나 내부 프로세스 명세가 아닙니다.

즉, 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

스트림 키의 정체

스트림 키는 ingest 요청을 프로젝트·채널 설정에 연결하는 인증 정보입니다. 내부 payload 형식과 UID 인코딩 방식은 공개 계약으로 확인되지 않으므로 디코딩 가능한 구조라고 가정하지 않습니다.

스트림 키 → 서비스가 검증 → 구성된 Agora 채널로 publish

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

실제 배포에서 passthrough인지 transcode인지는 Media Gateway의 현재 configuration과 입력·출력 codec 호환성으로 확인해야 합니다. 사례 당시에는 probe 결과와 설정을 통해 재인코딩 없는 경로로 판단했습니다.

패스스루 장점

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

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

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

  • downstream에서 협상한 H.264 profile·packetization mode와 입력 bitstream이 맞는지 확인해야 함
  • B-picture가 reorder latency를 늘리거나 특정 gateway/decoder 경로의 호환성 문제를 드러낼 수 있음
  • 뒤에서 설명할 "Main + bframes=0" 처방이 여기서 작동합니다 → 자세한 이유는 §7 해결 · Profile·인코더 옵션 가이드 참조

트랜스코딩이 필요한 경우

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

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

단점은 CPU·비용, 추가 지연, generation loss입니다. 구체적인 지연은 encoder·해상도·hardware·queueing에 따라 측정해야 합니다. 송출단을 통제할 수 있으면 downstream 협상과 맞는 bitstream을 직접 만들고, 통제할 수 없으면 edge transcoding을 검토합니다.


2. 증상 요약

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

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


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

H.264 elementary stream 자체가 각 picture에 DTS와 PTS를 저장하는 것은 아닙니다. bitstream에는 POC(Picture Order Count) 등 출력 순서를 복원할 정보가 있고, MP4·MPEG-TS·FLV 같은 container가 decode timestamp와 composition/presentation time을 표현합니다.

용어풀네임의미
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 header에는 별도 DTS field가 없습니다. 그렇다고 B-picture와 RTP가 표준상 충돌하는 것은 아닙니다.


4. RTP timestamp와 decoding order

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).

RFC 3550은 sampling order와 transmission order가 다르면 연속 RTP packet의 timestamp가 non-monotonic할 수 있다고 명시합니다. RFC 6184도 H.264 packetization mode와 decoding order를 정의합니다. 따라서 "timestamp가 뒤로 갔다"는 사실만으로 규격 위반이나 B-picture 불가를 결론 내릴 수 없습니다.

이 사례에서 검증해야 할 경계는 RTMP composition time을 RTP timestamp로 바꾸는 gateway, negotiated packetization/profile, 그리고 receiver reorder queue입니다. 사례 당시 로그와 A/B 테스트는 그 구현 경로가 입력 B-picture 구조를 안정적으로 처리하지 못했다는 근거이지, RTP 일반의 제약을 증명하지는 않습니다.


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

B-picture는 저장형 미디어에서 중요한 압축 도구입니다. 저지연 WebRTC encoder 설정에서는 reorder delay, loss recovery, 협상 호환성 때문에 흔히 비활성화하지만 구현과 codec에 따라 선택은 달라집니다.

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 지연 발생

추가 지연은 frame period와 reorder depth에 비례합니다. 예를 들어 30fps에서 future reference 한 장을 기다리면 약 33ms의 capture-side 대기가 생길 수 있습니다. 이것이 허용 가능한지는 전체 latency budget으로 판단합니다.

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

P-frame만 있을 때:

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

B-slice는 list 0, list 1 또는 양쪽 reference list를 사용할 수 있고, 일부 B-picture는 다른 picture의 reference가 될 수도 있습니다. 따라서 단순히 "앞뒤를 반드시 모두 참조"한다고 일반화할 수 없습니다.

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

유실 영향은 실제 reference graph와 concealment·RTX/FEC에 달려 있습니다. PLI는 손실 packet 자체의 재전송 요청이 아니라 decoder refresh picture가 필요하다는 feedback입니다.

5-3. gateway와 receiver의 reorder 지원

RTP timestamp의 non-monotonicity는 RFC 3550이 허용합니다. 문제는 각 gateway와 receiver가 negotiated H.264 packetization mode 안에서 decoding order와 output order를 끝까지 보존하는지입니다. 공개 표준에서 허용된다는 사실과 특정 제품 경로가 지원한다는 사실은 별개입니다.

5-4. libwebrtc의 codec profile 정책

WebRTC endpoint는 H.264 profile을 SDP의 profile-level-id로 협상합니다. RFC 7742의 필수 구현 profile은 Constrained Baseline이며, 다른 profile 지원은 endpoint별로 다릅니다. 실제 선택은 offer/answer의 fmtp를 확인해야 합니다.

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

송출 bitstream은 협상된 profile과 level 제약을 지켜야 합니다. 맞지 않는 Main/High bitstream을 그대로 주입하면 정상 상호운용을 기대할 수 없습니다.

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

하드웨어 decoder 지원 범위는 기기·OS·profile·level에 따라 다릅니다. 따라서 일반적인 모바일 최적화 주장을 근거 없이 적용하기보다 capability negotiation과 대상 기기 테스트로 확인합니다.

5-6. 요약

요인임팩트
Look-ahead 지연실시간 지연 예산 초과
참조 체인 확대유실 시 복구 비용 폭증
gateway/receiver reorder 지원특정 변환 경로에서 timestamp·decoding order 처리 실패 가능
libwebrtc profile 기본값Constrained Baseline 협상 시 B-frame 금지
하드웨어 디코더 호환성모바일 케이스에서 드롭 위험

결론적으로 B-picture는 VOD 압축 효율과 low-latency·interop 사이의 tradeoff입니다. RTMP ingest를 WebRTC endpoint에 연결할 때는 input bitstream이 downstream의 negotiated capability와 맞는지 별도로 검증해야 합니다.


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 → 정상

같은 경로에서 B-picture 설정만 바꾼 A/B 결과는 송출 bitstream과 downstream 호환성이 강한 원인 후보라는 근거가 됐습니다. 네트워크나 중간 component의 B-picture 처리 결함까지 이 한 번의 테스트로 배제한 것은 아닙니다.

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

이 sample에서는 증상이 재현되지 않았습니다. source bitstream과 mux timestamp가 OBS 입력과 달랐으므로, 이 결과만으로 relay layer를 배제하지 않고 ffprobe -show_frames와 packet capture를 함께 비교했습니다.

Test 4 — Relay 레이어 추가

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

중간 relay hop을 한 단계 더 추가해도 같은 MP4 source에서는 재현되지 않았습니다. 이는 hop 수 자체보다 source bitstream 조건이 증상과 더 강하게 연관됐다는 증거입니다.

테스트 매트릭스

#시나리오증상
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

downstream이 Constrained Baseline을 협상한 경우 가장 직접적으로 맞출 수 있는 선택입니다. B-slice를 쓰지 않더라도 packetization mode·level·SPS/PPS 전달 등 다른 상호운용 조건은 별도로 확인해야 합니다.

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은 CABAC 등 일부 coding tool을 사용할 수 없어 같은 품질에서 더 많은 bitrate가 필요할 수 있습니다. 차이는 content와 encoder 설정으로 측정합니다.

Option B — Main + bframes=0

CABAC을 사용할 수 있는 Main profile에서 B-picture만 제거하는 구성입니다. 단, downstream이 Main profile을 협상하고 해당 level·packetization mode를 지원할 때만 사용합니다. 8x8 transform은 High profile 계열의 도구라 Main에서는 사용할 수 없습니다.

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 settings에 bframes=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가 0보다 크면 FFmpeg가 presentation reordering이 필요한 stream으로 판정했다는 뜻입니다. 실제 picture type·PTS/DTS는 -show_frames 또는 -show_packets로 추가 확인합니다.


9. Bitrate를 함께 낮춘 이유

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

  1. 재패킷화 오버헤드: RTP/UDP/IP·SRTP·extension과 선택적 FEC/RTX 비용이 추가됩니다. 비율은 packet size, IP version, loss와 복구 정책으로 계산합니다.
  2. burst 여유: 평균 bitrate가 link capacity 아래여도 VBV와 sender pacing에 따라 짧은 burst가 생길 수 있습니다. 실제 packet capture와 interface counter로 확인합니다.

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, 유실 흡수/복구 필요
타임스탬프container가 DTS/PTS 표현RTP timestamp + codec별 decoding order 규칙
코덱 프로파일서비스 설정에 따라 Main/High 등SDP에서 profile·level 협상
GOP 구조긴 GOP + B-frame 허용짧은 GOP + P-frame 중심
버퍼링수 초 버퍼jitter buffer 수십 ms

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


정리

  • 증상: RTMP → Agora Media Gateway → WebRTC 경로에서 영상 freeze, timestamp rollback 로그
  • 원인 후보: 이 사례에서는 Main profile 자체가 아니라 B-picture가 포함된 input과 gateway/receiver 호환성의 조합
  • 메커니즘: container의 decode/output order를 RTP/H.264 packetization으로 옮기는 구현 경계에서 reorder 처리가 실패한 것으로 추정
  • 저지연 WebRTC에서 B-picture를 줄이는 이유: reorder delay, 복잡한 reference graph, profile 협상과 구현 호환성
  • 해결: downstream이 협상한 profile에 맞추고 bframes=0으로 재검증. bitrate 하향은 별도의 capacity margin 조치
  • 격리 방법: 중간 홉을 교체하며 동일 증상 재현 여부로 책임 경계 검증

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


관련 글

참고 자료

© 2026 Frank Kim. All rights reserved.