전화 음성에서 웹 자막까지 4 — RTP 헤더와 PCMU·PCMA 분석
PT·sequence·timestamp·SSRC를 한 필드씩 읽고 G.711 20ms의 샘플 수와 바이트 수를 계산합니다. 패킷 손실과 재정렬, 캡처 누락을 구분합니다.
목차(24개 항목)
전화 연결 메시지가 정상이어도 소리가 끊기거나 한쪽만 들릴 수 있습니다. SIP가 “통화를 시작하자”고 합의하는 제어 프로토콜이라면, RTP는 실제 음성 조각을 운반합니다. 이 글은 RTP 헤더의 네 필드만으로 패킷의 정체와 시간 흐름을 읽는 연습입니다.
실습 상태: 아래 Wireshark RTP 캡처와 Stream Analysis는 아직 실행하지 않은 후속 실습입니다. 저장소에는 과거 실제 통화의 Gateway 통계와 단위 테스트 기록이 있지만, 이 글의 패킷 번호·Wireshark 화면·예상 출력은 설명용입니다. 새 캡처를 만들거나 실제 통화를 시작하지 않고 글만 작성했습니다.
시리즈 목차 #58 · 이전: Wireshark로 SIP 읽기 #60 · 다음: Python RTP Gateway #62
이번 글의 한 가지 개념: 도착 시각과 재생 시각은 다르다
UDP 패킷은 네트워크 상황에 따라 늦거나 순서가 바뀌어 도착할 수 있습니다. RTP는 이를 직접 복구해 주는 프로토콜이 아니라, 수신기가 판단할 수 있도록 sequence number와 timestamp 같은 정보를 헤더에 담습니다.
도착 시간은 “패킷이 내 컴퓨터에 언제 왔는가”이고, 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바이트입니다.
| 필드 | 크기 | 무엇을 답하나 |
|---|---|---|
| Payload Type(PT) | 7 bit | payload를 어떤 형식으로 해석할까? |
| Sequence Number | 16 bit | 빠지거나 중복·역전된 패킷이 있나? |
| Timestamp | 32 bit | 이 payload는 미디어 시간축 어디에 놓이나? |
| SSRC | 32 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 PT | clock rate | 한 샘플의 압축 payload |
|---|---|---|---|---|
| PCMU | G.711 μ-law, ulaw | 0 | 8,000 Hz | 1 byte |
| PCMA | G.711 A-law, alaw | 8 | 8,000 Hz | 1 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이라는 뜻입니다.
G.711 payload는 sample당 1바이트이므로 20ms 음성 payload는 흔히 160바이트입니다.
따라서 같은 SSRC의 연속 20ms PCMA/PCMU 패킷이라면 설명용으로 다음과 같은 패턴을 기대할 수 있습니다.
| 패킷 | sequence | timestamp | timestamp 증가 | payload 길이 |
|---|---|---|---|---|
| A | 32000 | 800000 | - | 160 B |
| B | 32001 | 800160 | 160 | 160 B |
| C | 32002 | 800320 | 160 | 160 B |
이것은 이 프로젝트의 20ms 구성에 맞춘 예시입니다. RTP 표준이 모든 G.711 패킷을 반드시 20ms·160바이트로 강제하는 것은 아닙니다. 실제 packetization time은 SDP와 송신 구현을 확인해야 합니다.
sequence로 손실과 순서 변경 읽기
정상적인 연속 패킷:
하나가 보이지 않는 경우:
이때 “네트워크에서 영구 손실됐다”고 즉시 단정하면 안 됩니다. 캡처 지점에서만 빠졌거나 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입니다.
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를 증가시킵니다.
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 순서로 일정하게 꺼냅니다.
버퍼를 크게 하면 순서 변경과 늦은 패킷을 받아들일 여지가 늘지만 지연이 커집니다. 작게 하면 지연은 줄지만 늦은 패킷을 버릴 가능성이 커집니다. 현재 Gateway의 기본 jitter_ms=40과 200ms 샘플 상한은 이 구현의 정책이지 RTP 표준의 고정값이 아닙니다.
deadline까지 샘플이 없을 때 현재 Gateway는 고급 PLC(Packet Loss Concealment)가 아니라 PCM 0을 넣는 단순 무음 보충을 사용합니다. 따라서 frames_synthesized가 증가하면 끊김을 숨겼다는 뜻이 아니라 누락 구간을 무음으로 채운 증거입니다.
실습 하나: 한 RTP stream의 네 필드 비교하기
이 절은 앞으로 사용자가 수행할 실습이며 블로그 작성 과정에서는 실행하지 않았습니다.
1단계 — VM에서 SIP와 RTP를 함께 캡처
실행 위치: Ubuntu VM의 새 터미널
이 capture filter는 다음을 포함합니다.
| 범위 | 이 프로젝트에서 기대하는 구간 |
|---|---|
| UDP 5060 | Linphone ↔ Asterisk SIP |
| UDP 10000–10020 | Asterisk의 RTP 범위 |
| UDP 60000 | Asterisk External Media → Gateway loopback RTP |
사용자가 Linphone으로 7000에 한 번 전화해 짧게 말하고 끊은 뒤, 캡처 터미널에서 Ctrl+C로 종료합니다. 실제 인증값이나 pcap 파일은 공개 저장소에 올리지 않습니다.
2단계 — Mac Wireshark에서 SIP가 알려 준 RTP 찾기
캡처 파일을 Mac Wireshark에서 열고 먼저 sip || sdp display filter로 SDP의 IP·포트·PT를 확인합니다. 그 다음 다음 필터를 적용합니다.
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 분석과 재생 기능을 확인할 수 있습니다.
설명용 예상 해석
이 패턴이 보이면 “해당 구간에서 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와 수명·범위가 다릅니다. 서비스 사용자 데이터베이스의 기본 키로 쓰면 안 됩니다.
단계별 과제
- 같은 SSRC의 연속 패킷 세 개를 골라 네 필드를 표에 적습니다.
- timestamp 차이를 8,000으로 나눠 밀리초로 환산합니다.
- sequence gap이 있으면 앞뒤 도착 시각과 같은 SSRC인지 확인합니다.
- Linphone→Asterisk와 Asterisk→Gateway stream을 주소·포트·SSRC로 나눕니다.
- 관찰 결과를 “이 캡처 지점에서 확인한 사실”과 “추가 검증이 필요한 추론”으로 두 칸에 적습니다.
확인 문제
- sequence number와 timestamp는 각각 무엇을 세나요?
- 8 kHz G.711 20ms는 몇 sample인가요?
- PCMA와 PCMU의 static PT는 각각 무엇인가요?
- RTP SSRC와 Agora RTC UID는 같은 식별자인가요?
- RTP 패킷을 봤다는 사실만으로 웹 자막 성공을 증명할 수 있나요?
정답은 “패킷 순서와 미디어 clock 위치”, “160 sample”, “8과 0”, “아니다”, “아니다”입니다.
참고 자료
- RFC 3550 — RTP: A Transport Protocol for Real-Time Applications
- RFC 3551 — RTP Profile for Audio and Video Conferences
- RFC 4566 — SDP: Session Description Protocol
- Asterisk — External Media and ARI
- Wireshark — RTP Display Filter Reference
- Wireshark User’s Guide — RTP Analysis and RTP Player
시리즈 목차 #58 · 이전: Wireshark로 SIP 읽기 #60 · 다음: Python RTP Gateway #62