블로그 목록
실시간 통신15분 읽기

전화 음성에서 웹 자막까지 4 — RTP 헤더와 PCMU·PCMA 분석

PT·sequence·timestamp·SSRC를 한 필드씩 읽고 G.711 20ms의 샘플 수와 바이트 수를 계산합니다. 패킷 손실과 재정렬, 캡처 누락을 구분합니다.

PBX 입문RTPG.711PCMUPCMAWireshark

전화 연결 메시지가 정상이어도 소리가 끊기거나 한쪽만 들릴 수 있습니다. SIP가 “통화를 시작하자”고 합의하는 제어 프로토콜이라면, RTP는 실제 음성 조각을 운반합니다. 이 글은 RTP 헤더의 네 필드만으로 패킷의 정체와 시간 흐름을 읽는 연습입니다.

실습 상태: 아래 Wireshark RTP 캡처와 Stream Analysis는 아직 실행하지 않은 후속 실습입니다. 저장소에는 과거 실제 통화의 Gateway 통계와 단위 테스트 기록이 있지만, 이 글의 패킷 번호·Wireshark 화면·예상 출력은 설명용입니다. 새 캡처를 만들거나 실제 통화를 시작하지 않고 글만 작성했습니다.

시리즈 목차 #58 · 이전: Wireshark로 SIP 읽기 #60 · 다음: Python RTP Gateway #62

이번 글의 한 가지 개념: 도착 시각과 재생 시각은 다르다

UDP 패킷은 네트워크 상황에 따라 늦거나 순서가 바뀌어 도착할 수 있습니다. RTP는 이를 직접 복구해 주는 프로토콜이 아니라, 수신기가 판단할 수 있도록 sequence number와 timestamp 같은 정보를 헤더에 담습니다.

네트워크 도착 순서:  101 ── 103 ── 102
RTP sequence:        101    103    102   → 순서 변경을 발견
RTP timestamp:       0      320    160   → 재생 위치를 계산

도착 시간은 “패킷이 내 컴퓨터에 언제 왔는가”이고, RTP timestamp는 “미디어 시간축에서 payload 첫 샘플이 어디에 놓이는가”입니다. 둘을 구분해야 jitter buffer가 왜 필요한지 이해할 수 있습니다.

RFC 3550은 sequence number가 RTP 데이터 패킷마다 1씩 증가하고, timestamp가 payload 첫 octet의 sampling instant를 나타낸다고 정의합니다. timestamp의 단위는 초가 아니라 payload format이 정한 clock rate입니다.

최소 RTP 헤더에서 볼 네 필드

RTP 기본 헤더는 CSRC와 확장 헤더가 없을 때 최소 12바이트입니다.

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC  |M|     PT      |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         audio payload ...                     |
필드크기무엇을 답하나
Payload Type(PT)7 bitpayload를 어떤 형식으로 해석할까?
Sequence Number16 bit빠지거나 중복·역전된 패킷이 있나?
Timestamp32 bit이 payload는 미디어 시간축 어디에 놓이나?
SSRC32 bit어떤 RTP 동기화 소스에서 왔나?

SSRC는 SIP 계정 1001, dialplan 목적지 7000, Agora RTC UID, Agent UID가 아닙니다. RTP 세션 안의 synchronization source 식별자입니다. 우연히 숫자가 같을 수는 있어도 동일한 식별 체계로 취급하면 안 됩니다.

이 구성의 PCMU와 PCMA

RFC 3551은 기본 RTP/AVP profile의 static payload type을 정의합니다. 실제 통화 해석에서는 SDP 협상 내용을 먼저 확인합니다. 다른 profile이나 동적 매핑까지 PT 숫자만으로 일반화하면 안 됩니다.

코덱다른 이름RTP PTclock rate한 샘플의 압축 payload
PCMUG.711 μ-law, ulaw08,000 Hz1 byte
PCMAG.711 A-law, alaw88,000 Hz1 byte

현재 Asterisk 설치 스크립트는 AST_CODEC을 ulaw 또는 alaw 중 하나로 제한합니다. Gateway 설정도 같은 코덱이어야 합니다. alaw로 설정했는데 PT 0이 오면 payload 바이트만 보고 자동 추측하지 않고 거부하는 것이 현재 코드의 정책입니다.

G.711은 선형 PCM을 8-bit companded 값으로 표현하는 손실 변환입니다. 디코딩 뒤 16-bit PCM 샘플을 얻지만 원래 입력의 모든 비트를 복원하는 무손실 압축은 아닙니다.

20ms 패킷의 숫자를 직접 계산한다

8 kHz는 1초에 8,000 sample이라는 뜻입니다.

8,000 samples/second × 0.020 second = 160 samples

G.711 payload는 sample당 1바이트이므로 20ms 음성 payload는 흔히 160바이트입니다.

G.711: 160 samples × 1 byte = 160 bytes
PCM S16LE: 160 samples × 2 bytes = 320 bytes

따라서 같은 SSRC의 연속 20ms PCMA/PCMU 패킷이라면 설명용으로 다음과 같은 패턴을 기대할 수 있습니다.

패킷sequencetimestamptimestamp 증가payload 길이
A32000800000-160 B
B32001800160160160 B
C32002800320160160 B

이것은 이 프로젝트의 20ms 구성에 맞춘 예시입니다. RTP 표준이 모든 G.711 패킷을 반드시 20ms·160바이트로 강제하는 것은 아닙니다. 실제 packetization time은 SDP와 송신 구현을 확인해야 합니다.

sequence로 손실과 순서 변경 읽기

정상적인 연속 패킷:

4100 → 4101 → 4102 → 4103

하나가 보이지 않는 경우:

4100 → 4101 → 4103
                ▲
              4102 미관찰

이때 “네트워크에서 영구 손실됐다”고 즉시 단정하면 안 됩니다. 캡처 지점에서만 빠졌거나 display filter에서 숨겼거나, 나중에 늦게 도착할 수도 있습니다. 전체 캡처와 양방향 stream을 확인한 뒤 loss 또는 late로 분류합니다.

16-bit sequence는 65535 다음 0으로 돌아갑니다. 따라서 65535 → 0은 그 자체로 손실이나 세션 재시작이 아닙니다. 현재 Python Gateway도 wrap-around를 연속된 긴 번호처럼 풀어서 처리합니다.

timestamp로 시간과 누락 샘플 읽기

PCMA/PCMU의 8 kHz clock에서 timestamp 160 증가는 20ms입니다.

160 samples ÷ 8,000 samples/second = 0.020 second

sequence는 1 증가했는데 timestamp가 320 증가했다면 가능한 원인은 여러 가지입니다.

  • 송신기가 한 패킷에 40ms 단위로 구성했을 수 있음
  • 중간 20ms 구간의 패킷을 캡처하지 못했을 수 있음
  • DTX나 구현별 timestamp 정책이 작동했을 수 있음
  • 서로 다른 SSRC나 stream을 섞어 보고 있을 수 있음

하나의 숫자만 보고 원인을 확정하지 않고 SDP, payload 길이, SSRC, 연속 구간을 함께 봅니다.

SSRC가 바뀌면 왜 주의해야 하나

RFC 3550에서 SSRC는 synchronization source를 식별합니다. 같은 UDP 주소에서 SSRC가 바뀌면 수신기는 새 소스나 충돌 해결 상황을 고려해야 합니다. 현재 Gateway 구현은 SSRC 변경을 발견하면 타임라인을 초기화하고 ssrc_resets를 증가시킵니다.

peer 127.0.0.1:포트, SSRC 0x11111111 ── 기존 타임라인
peer 127.0.0.1:포트, SSRC 0x22222222 ── 새 스트림으로 초기화

Asterisk가 Linphone의 RTP를 External Media RTP로 다시 만들어 Gateway에 보내므로, Linphone→Asterisk와 Asterisk→Gateway 구간의 sequence·timestamp·SSRC가 그대로 보존된다고 가정하지 않습니다. 두 구간은 별도 RTP stream으로 분석합니다. Asterisk 공식 External Media and ARI 문서에 따르면 External Media 채널의 format은 생성할 때 지정하며 그 채널 안에서 별도 코덱 협상을 하지 않습니다. 따라서 SIP leg의 SDP 협상 결과와 External Media에 지정한 format을 각각 확인해야 합니다.

jitter buffer는 무엇을 바꾸나

jitter buffer는 패킷을 잠깐 기다렸다가 timestamp 순서로 일정하게 꺼냅니다.

도착:  A ───── C ─ B
              │
버퍼:       [A B C]
              │
출력:  A ─ B ─ C  (일정한 20ms 간격)

버퍼를 크게 하면 순서 변경과 늦은 패킷을 받아들일 여지가 늘지만 지연이 커집니다. 작게 하면 지연은 줄지만 늦은 패킷을 버릴 가능성이 커집니다. 현재 Gateway의 기본 jitter_ms=40과 200ms 샘플 상한은 이 구현의 정책이지 RTP 표준의 고정값이 아닙니다.

deadline까지 샘플이 없을 때 현재 Gateway는 고급 PLC(Packet Loss Concealment)가 아니라 PCM 0을 넣는 단순 무음 보충을 사용합니다. 따라서 frames_synthesized가 증가하면 끊김을 숨겼다는 뜻이 아니라 누락 구간을 무음으로 채운 증거입니다.

실습 하나: 한 RTP stream의 네 필드 비교하기

이 절은 앞으로 사용자가 수행할 실습이며 블로그 작성 과정에서는 실행하지 않았습니다.

1단계 — VM에서 SIP와 RTP를 함께 캡처

실행 위치: Ubuntu VM의 새 터미널

cd /workspace/pbx_gateway
mkdir -p artifacts/training
sudo tcpdump -ni any -s 0 -w artifacts/training/sip-rtp-call.pcap \
  'udp port 5060 or udp portrange 10000-10020 or udp port 60000'

이 capture filter는 다음을 포함합니다.

범위이 프로젝트에서 기대하는 구간
UDP 5060Linphone ↔ Asterisk SIP
UDP 10000–10020Asterisk의 RTP 범위
UDP 60000Asterisk External Media → Gateway loopback RTP

사용자가 Linphone으로 7000에 한 번 전화해 짧게 말하고 끊은 뒤, 캡처 터미널에서 Ctrl+C로 종료합니다. 실제 인증값이나 pcap 파일은 공개 저장소에 올리지 않습니다.

2단계 — Mac Wireshark에서 SIP가 알려 준 RTP 찾기

캡처 파일을 Mac Wireshark에서 열고 먼저 sip || sdp display filter로 SDP의 IP·포트·PT를 확인합니다. 그 다음 다음 필터를 적용합니다.

rtp
rtp.p_type == 8
rtp.ssrc == 0x12345678

0x12345678은 문법을 보여 주는 예시입니다. 실제 패킷의 SSRC로 바꾸세요. 다른 값이면 결과가 0건일 수 있습니다.

Wireshark가 UDP를 RTP로 자동 해석하지 못하면 SDP가 캡처에 포함됐는지 먼저 확인합니다. 그래도 식별되지 않고 해당 UDP 5-tuple과 코덱을 증거로 확인했다면 Analyze → Decode As… → RTP를 사용할 수 있습니다. 아무 UDP나 RTP로 강제 해석하면 우연한 바이트가 그럴듯한 헤더로 보일 수 있습니다.

Wireshark의 공식 RTP display filter reference에서 rtp.seq, rtp.timestamp, rtp.ssrc, rtp.p_type 같은 필드를 확인할 수 있습니다.

3단계 — 연속 패킷 세 개를 표로 기록

같은 방향·같은 SSRC에서 연속 패킷 세 개를 골라 기록합니다.

관찰 항목패킷 A패킷 B패킷 C
frame number직접 기록직접 기록직접 기록
arrival time직접 기록직접 기록직접 기록
PT직접 기록직접 기록직접 기록
sequence직접 기록직접 기록직접 기록
timestamp직접 기록직접 기록직접 기록
SSRC직접 기록직접 기록직접 기록
payload length직접 기록직접 기록직접 기록

아직 실행 전이므로 실제 값 대신 빈 표를 남깁니다. 실습 후 관찰값으로 채워야 합니다.

4단계 — RTP Stream Analysis로 통계 확인

Telephony → RTP → RTP Streams에서 같은 방향 stream을 선택하고 Analyze를 엽니다. 패킷 수, sequence 오류, jitter, delta를 확인합니다. RTP Player는 파형과 재생을 제공하지만 음성에 개인정보가 포함될 수 있으므로 저장·공유 범위를 먼저 정합니다. 공식 Wireshark RTP Player 문서에서 stream 분석과 재생 기능을 확인할 수 있습니다.

설명용 예상 해석

PT: 8
Sequence: 패킷마다 대체로 +1
Timestamp: 20ms G.711 패킷이면 대체로 +160
SSRC: 한 stream 안에서 동일
Payload: 20ms G.711이면 흔히 160 bytes

이 패턴이 보이면 “해당 구간에서 PCMA로 보이는 연속 RTP stream을 관찰했다”고 말할 수 있습니다. 이것만으로 전체 통화 음질, 반대 방향 음성, Agora 수신, STT 성공까지 증명하지는 못합니다.

결과 해석: 증거의 범위를 넘지 않는다

관찰말할 수 있는 것아직 말할 수 없는 것
SIP 200/ACK통화 제어가 성립한 정황RTP가 도착함
RTP 패킷 증가해당 캡처 지점에 미디어가 도착함사용자가 정상 청취함
PT 8 + SDP PCMA해당 stream을 PCMA로 해석할 근거PCM 변환과 Agora 게시 성공
sequence gap패킷 누락 또는 캡처 누락 후보네트워크 영구 손실로 확정
RTP Player에서 음성캡처된 한 방향 payload를 디코딩 가능전체 제품의 양방향 품질 합격

Gateway의 arrival_to_output_p95_ms도 범위를 주의해야 합니다. 이는 RTP 도착에서 Gateway 출력 경계까지의 지표이며 RTC 전송, Agora STT 인식, Signaling 전달, 브라우저 표시를 합친 end-to-end 자막 지연이 아닙니다.

흔한 오류

rtp display filter 결과가 없다

패킷이 없다는 뜻과 Wireshark가 RTP로 해석하지 못했다는 뜻을 구분합니다. 먼저 UDP 패킷, SDP의 미디어 포트, 5-tuple을 확인한 뒤 필요한 경우에만 Decode As를 사용합니다.

PT 8을 보고 모든 구간이 PCMA라고 결론냈다

PT는 RTP stream 문맥에서 해석합니다. Linphone→Asterisk와 Asterisk→Gateway는 별도 stream이며, 각 SDP·설정·패킷을 확인해야 합니다. dynamic PT는 SDP 매핑 없이는 코덱을 확정할 수 없습니다.

timestamp를 Unix 시간으로 읽었다

RTP timestamp는 wall-clock 날짜가 아닙니다. PCMA/PCMU에서는 8 kHz media clock의 샘플 위치입니다. 실제 시각과 동기화할 때는 RTCP Sender Report 등의 별도 매핑이 필요합니다.

sequence gap 하나를 음질 장애로 확정했다

캡처 누락, 패킷 순서 변경, 늦은 도착, 반대 방향 stream 혼합 가능성을 먼저 제거합니다. 실제 청취와 수신기 통계도 함께 봅니다.

SSRC를 사용자 ID로 사용했다

SSRC는 RTP 동기화 소스 식별자입니다. SIP endpoint, 전화번호, Agora UID와 수명·범위가 다릅니다. 서비스 사용자 데이터베이스의 기본 키로 쓰면 안 됩니다.

단계별 과제

  1. 같은 SSRC의 연속 패킷 세 개를 골라 네 필드를 표에 적습니다.
  2. timestamp 차이를 8,000으로 나눠 밀리초로 환산합니다.
  3. sequence gap이 있으면 앞뒤 도착 시각과 같은 SSRC인지 확인합니다.
  4. Linphone→Asterisk와 Asterisk→Gateway stream을 주소·포트·SSRC로 나눕니다.
  5. 관찰 결과를 “이 캡처 지점에서 확인한 사실”과 “추가 검증이 필요한 추론”으로 두 칸에 적습니다.

확인 문제

  1. sequence number와 timestamp는 각각 무엇을 세나요?
  2. 8 kHz G.711 20ms는 몇 sample인가요?
  3. PCMA와 PCMU의 static PT는 각각 무엇인가요?
  4. RTP SSRC와 Agora RTC UID는 같은 식별자인가요?
  5. RTP 패킷을 봤다는 사실만으로 웹 자막 성공을 증명할 수 있나요?

정답은 “패킷 순서와 미디어 clock 위치”, “160 sample”, “8과 0”, “아니다”, “아니다”입니다.

참고 자료

시리즈 목차 #58 · 이전: Wireshark로 SIP 읽기 #60 · 다음: Python RTP Gateway #62

© 2026 Frank Kim. All rights reserved.