RTCP란 무엇인가 — SR/RR/NACK/PLI 기초

kodeholic·2026년 3월 28일

RTCP 완전정복

목록 보기
1/5
post-thumbnail

RTCP 완전정복 시리즈 #1

화상회의 중에 상대방 영상이 1초간 멈췄다. 그런데 보낸 쪽은 이걸 모른다.

UDP는 "보내면 끝"이다. 패킷이 도착했는지, 순서가 맞는지, 몇 개나 빠졌는지 — 아무것도 알려주지 않는다. TCP처럼 ACK을 주고받는 구조가 아니기 때문이다.

RTP도 마찬가지다. RTP는 미디어 패킷을 실어 나르는 역할만 한다. 영상 프레임을 쪼개서 UDP에 얹어 보내되, 그게 잘 도착했는지는 관심 없다.

문제는, 실시간 통신에서 "잘 도착했는지"를 모르면 품질 제어가 불가능하다는 것이다. 해상도를 낮춰야 할지, 비트레이트를 줄여야 할지, 특정 패킷을 재전송해야 할지 — 판단의 근거가 없다.

그래서 RTP에는 짝꿍이 있다. RTCP (RTP Control Protocol). 미디어를 나르는 건 RTP가 하고, "잘 받았는지" 피드백하는 건 RTCP가 한다. RFC 3550에서 RTP와 함께 정의된, 같은 세션의 제어 채널이다.

택배에 비유하면 이렇다. RTP는 택배 트럭이고, RTCP는 수령 확인서다. 트럭은 상자를 실어 나르기만 하고, 수령 확인서가 "12개 중 10개 받았고 2개 파손"이라고 보고한다. 이 보고가 없으면 택배 회사는 배송 품질을 개선할 방법이 없다.

RTCP에는 여러 종류의 메시지가 있다. 이번 글에서는 실시간 통신에서 가장 자주 만나는 4가지를 다룬다.


1. SR — 보낸 쪽의 보고서

화상회의에서 상대방의 입이 움직이는데 목소리가 0.5초 늦게 들린다고 상상해보자. 영상과 음성이 따로 논다. 이걸 막으려면, 받는 쪽이 "오디오 타임스탬프 X와 비디오 타임스탬프 Y가 실제로는 같은 시각이다"라는 기준점을 알아야 한다.

이 기준점을 알려주는 게 SR(Sender Report)이다. 보내는 쪽이 주기적으로 "지금 벽시계 시각은 14:30:05.123이고, 이 시점의 RTP 타임스탬프는 928,000이다"라고 보고한다.

  [보내는 쪽]                           [받는 쪽]
      |                                    |
      |  RTP 패킷 (영상/음성)               |
      |----------------------------------->|
      |                                    |
      |  SR (Sender Report)                |
      |----------------------------------->|
      |   "지금 시각은 14:30:05.123,        |
      |    총 4,521패킷 보냈어"             |
      |                                    |

오디오와 비디오는 각자 다른 클록 주파수를 쓴다(오디오 48kHz, 비디오 90kHz). 타임스탬프 숫자만 보면 둘을 맞출 방법이 없다. SR이 "벽시계 기준 몇 시 몇 분"이라는 공통 시간을 찍어주기 때문에, 받는 쪽이 오디오와 비디오의 싱크를 맞출 수 있는 거다. 흔히 말하는 lip sync의 기준점이 바로 이 SR이다.

SR에는 "지금까지 총 몇 패킷, 몇 바이트 보냈다"는 통계도 들어있다. 받는 쪽은 이 숫자와 자기가 실제로 받은 수를 비교해서 손실률을 계산한다.


2. RR — 받는 쪽의 성적표

SR은 보내는 쪽이 자기 상태를 알려주는 거다. 그런데 보내는 쪽은 정작 가장 중요한 걸 모른다 — "내가 보낸 패킷이 상대방한테 잘 도착하고 있는가?"

이걸 알려주는 게 RR(Receiver Report)이다. 받는 쪽이 보내는 쪽에게 "네가 보낸 거 이렇게 받고 있어"라고 보고한다.

  [보내는 쪽]                           [받는 쪽]
      |                                    |
      |  RTP 패킷 (영상/음성)               |
      |----------------------------------->|
      |                                    |
      |            RR (Receiver Report)    |
      |<-----------------------------------|
      |   "loss: 3%, jitter: 12ms,         |
      |    마지막 SR 받고 40ms 걸렸어"       |
      |                                    |

RR이 전달하는 건 결국 "네트워크 상태의 성적표"다.

패킷 손실률 — "직전 보고 이후 3%가 안 왔어." 이 수치가 올라가면 네트워크가 혼잡하다는 신호다. 보내는 쪽은 이걸 보고 비트레이트를 낮추는 판단을 할 수 있다.

지터(jitter) — 패킷 도착 간격의 흔들림이다. 오디오 패킷이 20ms 간격으로 와야 하는데, 15ms 만에 오다가 35ms 만에 오다가 하면 지터가 높은 것이다. 받는 쪽의 버퍼(jitter buffer)가 이 흔들림을 흡수하지만, 한계를 넘으면 소리가 끊긴다.

RTT 계산용 타임스탬프 — SR을 받은 시각과 RR을 보내는 시각을 같이 기록해서, 보내는 쪽이 왕복 지연(Round Trip Time)을 역산할 수 있게 한다.

SR과 RR이 짝으로 오가는 구조다. SR은 "내가 뭘 보냈는지", RR은 "네가 보낸 게 어떻게 도착했는지". 이 두 보고서가 보통 5초 간격으로 교환되면서, 양쪽이 네트워크 상태를 공유한다.


3. NACK — 이 패킷 다시 보내줘

SR과 RR은 전체적인 품질 보고서다. "지난 5초간 3% 손실이 있었어." 유용하지만 느리다. 패킷이 하나 빠졌을 때 5초를 기다렸다가 보고하면, 그 사이에 화면은 이미 깨져 있다.

NACK(Negative Acknowledgement)은 즉각적이다. 특정 패킷 번호를 지목해서 "이거 안 왔으니 지금 다시 보내"라고 요청한다.

  [보내는 쪽]                           [받는 쪽]
      |   pkt #100                         |
      |----------------------------------->|  ✓ 수신
      |   pkt #101                         |
      |----------X  (손실)                  |
      |   pkt #102                         |
      |----------------------------------->|  ✓ 수신
      |                                    |
      |   "pkt #101 다시 보내줘" (NACK)     |
      |<-----------------------------------|
      |                                    |
      |   pkt #101 (재전송, RTX)            |
      |----------------------------------->|  ✓ 복구

받는 쪽은 패킷 번호(sequence number)를 보고 있다가, #100 다음에 #102가 오면 #101이 빠진 걸 즉시 안다. 그러면 NACK을 보내고, 보낸 쪽은 최근 패킷을 캐시해뒀다가 해당 패킷을 재전송한다. 이 재전송 패킷을 RTX(Retransmission)라고 부른다.

주의할 점이 하나 있다. NACK으로 복구할 수 있는 건 단발성 손실뿐이다. 네트워크가 심하게 혼잡해서 연속으로 수십 패킷이 날아가면, 하나하나 재전송 요청하는 건 불가능하다. 재전송 자체가 대역폭을 잡아먹어서 혼잡을 악화시킬 수도 있다.

그래서 "NACK으로 못 살리는 손실"이 있다. 특히 비디오에서.


4. PLI — 키프레임 하나 보내줘

비디오 코덱(VP8, H.264 등)은 매 프레임을 완전한 이미지로 보내지 않는다. 첫 프레임(I-frame, 키프레임)만 전체 이미지를 담고, 이후 프레임(P-frame)은 이전 프레임과의 차이만 담는다. 이렇게 하면 데이터양이 크게 줄어든다.

문제는, 이 구조가 체인처럼 연결되어 있다는 것이다.

  [I-frame] → [P] → [P] → [P] → [P] → [P] → ...
     전체       차이    차이    차이    차이    차이
     이미지

P-frame 하나가 빠지면? 그 뒤의 모든 P-frame이 "이전 프레임"을 참조할 수 없으므로 디코딩이 깨진다. 화면에 녹색 블록이 뜨거나, 일부가 뭉개지거나, 아예 멈춘다.

NACK으로 빠진 패킷을 복구하면 해결되지만, 패킷이 여러 개 연속으로 빠지거나 I-frame 자체가 유실되면 NACK만으로는 답이 없다. 이때 쓰는 게 PLI(Picture Loss Indication)다.

  [보내는 쪽]                           [받는 쪽]
      |   I-frame                          |
      |----------------------------------->|  ✓ 디코딩
      |   P-frame                          |
      |----------X  (손실)                  |
      |   P-frame                          |
      |----------------------------------->|  ✗ 참조 프레임 없음
      |                                    |     → 화면 깨짐
      |   "I-frame 하나 보내줘" (PLI)       |
      |<-----------------------------------|
      |                                    |
      |   I-frame (새로 생성)               |
      |----------------------------------->|  ✓ 화면 복구

PLI를 받으면 보내는 쪽의 인코더가 새 키프레임을 즉시 생성한다. 받는 쪽은 이 키프레임부터 다시 참조 체인을 시작하므로, 화면이 복구된다.

대신 비용이 있다. 키프레임은 P-frame보다 5~10배 크다. PLI를 너무 자주 보내면 갑자기 대역폭이 치솟고, 다른 사람의 품질까지 영향을 받는다. 이건 뒤에서 자세히 다루겠지만, "PLI는 공짜가 아니다"라는 점만 기억해두면 된다.


한눈에 비교

메시지방향역할주기비용
SR송신→수신송신 통계 + 시간 매핑주기적 (~5초)낮음
RR수신→송신수신 품질 (loss, jitter)주기적 (~5초)낮음
NACK수신→송신특정 패킷 재전송 요청즉시 (손실 감지 시)중간
PLI수신→송신키프레임 재생성 요청즉시 (디코딩 실패 시)높음

SR/RR은 전체 건강 상태 보고서이고, NACK/PLI는 즉각적인 응급 처치다.

실제 통화에서는 이 네 가지가 동시에 돌아간다. 5초마다 SR/RR이 전체 현황을 보고하고, 그 사이에 패킷이 빠지면 NACK이 즉시 재전송을 요청하고, NACK으로 안 되면 PLI가 키프레임을 요청한다.


그런데, SFU가 끼면?

P2P 통화에서는 간단하다. A가 B에게 보내고, B가 A에게 RTCP를 돌려보내면 끝이다.

  P2P: 깔끔한 1:1 피드백

  [A] ---- RTP ----> [B]
  [A] <--- RTCP ---- [B]

그런데 SFU가 중간에 있으면 상황이 복잡해진다. A가 SFU에 보내고, SFU가 B, C, D에게 복사 전달한다. B가 "패킷 3개 잃어버렸어"라고 RTCP를 보내면 — 이걸 누가 받아야 하는가? SFU인가, A인가?

  SFU: 누가 RTCP를 처리하는가?

  [A] ---- RTP ----> [SFU] ---- RTP ----> [B]
                       |                    |
                       |  ---- RTP ----> [C]
                       |                    |
                       |   ??? <--- RTCP ---|

B의 RR을 A에게 그대로 전달하면 될까? B가 보낸 NACK을 SFU가 직접 처리해야 할까? A가 보낸 SR을 B에게 그대로 릴레이하면 될까?

답은 "그대로 전달하면 안 된다"이다. SFU는 단순 릴레이가 아니라, 두 독립 세션의 종단점이다. 이게 무슨 의미인지, 왜 "그대로 전달"이 문제를 일으키는지 — 다음 편에서 다룬다.

다음 편: [RTCP 완전정복 #2] SFU는 relay가 아니다 — RTCP Terminator 설계


참고 자료


이 글은 OxLens — Rust로 만드는 경량 SFU 서버 프로젝트를 개발하면서 배운 것들을 정리한 시리즈입니다.

profile
Creator of 0xLENS — the world's first AI-native, Rust-built SFU with real-time PTT Floor Control. No fork. No framework. Just Rust and resolve.

0개의 댓글