FLV, TS, fMP4: 컨테이너마다 달라지는 NAL 유닛 포장 방식
같은 NAL 유닛인데 컨테이너가 바뀌면 왜 죽을까요. RTMP로는 멀쩡하던 스트림이 HLS에서 첫 프레임에 터지는 이유를 따라가 보겠습니다.
먼저 사건부터: 인제스트는 정상인데 배포에서 크래시가 난다
실제 운영 중이던 라이브 채널에서 있었던 일입니다.
- 인코더에서 서버로 올라오는 구간(RTMP): 플레이어로 확인하면 정상 재생됩니다.
- 같은 스트림을 HLS로 패키징한 뒤: 첫 프레임에서 디코더가 크래시합니다.
비트스트림 자체는 복사한 거나 다름없는데 결과가 갈렸습니다. 차이는 하나뿐이었습니다. NAL 유닛을 감싸는 포장 방식이 달라졌습니다.
RTMP / FLV -> [4바이트 길이][NAL] [4바이트 길이][NAL] ... (AVCC)
HLS / TS -> [00 00 00 01][NAL] [00 00 00 01][NAL] ... (Annex B)NAL 유닛의 내용은 그대로인데, "어디서부터 어디까지가 한 개의 NAL인지"를 표시하는 방법이 다릅니다. 이걸 모르고 페이로드만 그대로 옮기면 디코더는 경계를 잃어버립니다.
왜 컨테이너마다 포장 방식이 다른가
각 전송 계층이 자기한테 맞는 경계 표시법을 요구하기 때문입니다.
| 컨테이너 | 경계를 어떻게 아는가 | 그래서 어떤 포장을 쓰는가 |
|---|---|---|
| FLV 태그 | 태그 헤더에 길이가 이미 있습니다 | 길이 prefix (AVCC)로 충분합니다 |
| MPEG-TS | 188바이트로 쪼개지며, 중간부터 동기화해야 합니다 | 스트림 안에서 스스로 찾을 수 있는 Start Code (Annex B) |
| fMP4 | moof에 샘플 길이 표가 있습니다 | 길이 prefix (AVCC) |
핵심은 TS만 스트림 중간에서 자기 위치를 증명해야 한다는 점입니다. UDP로 방송을 쏘면 패킷이 뒤섞이고 손실도 생기는데, 이때 0x47로 시작하는 188바이트 경계를 훑어서 아무 데서나 동기화를 걸 수 있어야 합니다. AVCC처럼 "앞에 길이가 있다"는 약속은 한 바이트만 밀려도 무너집니다.
RTMP: FLV 태그로 포장한다
RTMP는 Flash Player 시대의 유산이지만, 여전히 **송출 구간(인코더에서 서버로)**에서 가장 많이 쓰입니다.
여기서 중요한 건 순서입니다. RTMP 연결이 수립되면 제일 먼저 AVC Decoder Config Record를 보냅니다. 이 안에 SPS/PPS가 들어 있습니다. 이게 없으면 플레이어는 해상도도 모르고, 프로파일도 모르고, 결국 디코더를 초기화할 수 없습니다. 그다음부터 이어지는 NAL들은 길이 prefix로만 경계를 표시합니다.
참고: RTMP에서 "오디오는 나오는데 영상이 안 나온다"는 제보가 오면, 의심 1순위는 이 Config Record가 유실됐거나 순서가 밀린 경우입니다.
RTMP는 TCP 기반이라 신뢰성은 높지만 지연이 큽니다. head-of-line blocking 때문에 한 패킷이 늦으면 뒤에 줄줄이 밀립니다. 그래서 요즘은 공용 인터넷 구간에서 RTMP 대신 SRT로 넘어가는 추세입니다.
SRT: UDP 위에서 TCP처럼
SRT는 UDP를 쓰면서도 TCP처럼 패킷 재전송을 지원합니다. SRT 패킷 안에는 188바이트 단위의 MPEG-TS 패킷이 들어 있고, 그 안에 PES를 거쳐 Annex B NAL 유닛들이 실립니다.
SRT의 장점은 세 가지입니다.
- UDP 기반이라 TCP보다 지연이 훨씬 작습니다.
- 패킷 손실을 감지해서 선택적으로 재전송하므로, 화질 손상을 최소화합니다.
- 공용 인터넷에서도 안정적인 기여도(contribution) 전송이 가능합니다.
큰 스포츠 이벤트나 현장 중계에서 SRT가 RTMP를 빠르게 대체하고 있는 이유가 여기 있습니다. 다만 SRT가 실어 나르는 건 결국 TS이고, TS 안의 NAL은 Annex B입니다. RTMP(AVCC)에서 SRT(Annex B)로 넘어가는 지점에서 다시 변환이 필요하다는 뜻입니다.
MPEG-TS: 방송의 표준이자 HLS의 근간
MPEG-TS는 188바이트 고정 길이 패킷으로 모든 데이터를 나릅니다.
| 구성 | 크기 | 역할 |
|---|---|---|
| Sync byte | 1바이트 (0x47) | 패킷 시작을 표시 |
| PID (Packet Identifier) | TS 헤더 내 | 비디오인지 오디오인지 구분 |
| PCR (Program Clock Reference) | TS 헤더 내 | 타이밍 기준 클럭 |
| 페이로드 | 184바이트 | PES를 거쳐 Annex B NAL 유닛 |
TS가 쓰이는 곳은 크게 세 가지입니다. HLS의 .ts 세그먼트 파일, DVB나 ATSC 같은 지상파/케이블 방송, 그리고 TS over SRT입니다.
참고: TS의 가장 큰 특징은 언제든지 중간부터 동기화할 수 있다는 것입니다. 바이트 스트림에서
0x47을 찾고, 188바이트 간격으로 다음0x47이 나오는지 확인하면 됩니다. UDP로 방송을 쏠 때 이 성질이 생명줄입니다.
한 가지 더 짚어두면, 184바이트 페이로드에 NAL 유닛이 딱 맞아떨어지는 일은 거의 없습니다. 그래서 하나의 NAL이 여러 TS 패킷에 걸쳐 쪼개지고, PES 헤더가 그 경계 정보를 들고 있습니다. 패키저가 이 PES 경계를 잘못 쓰면 디코더는 NAL이 끊긴 것으로 판단합니다.
fMP4(CMAF): HLS와 DASH를 하나로
기존 MP4는 moov(메타데이터)가 파일 맨 앞이나 맨 뒤에 몰려 있어서 스트리밍에 불편했습니다. 전체를 다 받기 전에는 재생을 시작할 수 없었기 때문입니다. **fMP4(Fragmented MP4)**가 이걸 해결했습니다.
| 방식 | 구조 | 특징 |
|---|---|---|
| 기존 MP4 | moov ... mdat ... mdat | 모든 메타데이터가 한곳에 몰려 있고, 미디어 데이터가 여기저기 흩어져 있습니다 |
| fMP4(CMAF) | moov [moof][mdat] [moof][mdat] ... | 초기화 뒤 세그먼트 단위로 moof+mdat가 반복됩니다 |
각 세그먼트는 moof(자기 자신의 메타데이터)와 mdat(미디어 데이터)로 독립적입니다. 세그먼트 하나만 받아도 그 안에서 어떻게 디코딩해야 하는지 알 수 있습니다. 게다가 같은 fMP4 세그먼트를 HLS(.m4s)와 DASH(.m4s)가 함께 쓸 수 있습니다. 이게 CMAF의 핵심입니다. 캐시 효율이 달라집니다.
fMP4 내부의 NAL 포장은 AVCC(길이 prefix)입니다. moof의 샘플 테이블에 각 NAL의 길이가 들어가기 때문에 굳이 Start Code가 필요 없습니다.
주의: 여기서 헷갈리기 쉬운 부분이 있습니다. fMP4와 MP4는 모두 AVCC지만, HLS는
.ts(Annex B)와.m4s(AVCC)를 둘 다 쓸 수 있습니다. 같은 HLS인데 컨테이너에 따라 NAL 포장이 달라진다는 뜻입니다. 매니페스트만 보고 판단하지 말고 실제 세그먼트를 열어 봐야 합니다.
한눈에 비교
| 프로토콜 | 전송 | NAL 포장 | 주 용도 |
|---|---|---|---|
| RTMP | TCP | AVCC (FLV 태그) | 송출, 기여도 구간 |
| SRT | UDP + 재전송 | Annex B (TS) | 송출, 공용망 장거리 |
| MPEG-TS | UDP/TCP | Annex B | 방송, HLS(.ts) |
| fMP4 / CMAF | HTTP | AVCC | HLS(.m4s), DASH(.m4s) |
정리하면 규칙은 단순합니다.
스트림 중간에서 동기화가 필요하다 -> Annex B (Start Code)
컨테이너가 길이를 이미 알고 있다 -> AVCC (길이 prefix)정리
- NAL 유닛 자체는 같고, 컨테이너가 바뀌면 경계 표시법만 달라집니다. 문제는 거의 항상 그 변환 지점에서 생깁니다.
- RTMP는 연결 직후 AVC Decoder Config Record로 SPS/PPS를 먼저 보냅니다. 이게 유실되면 디코더는 초기화조차 못 합니다.
- TS는 188바이트와
0x47sync byte 덕분에 스트림 중간 어디서든 동기화를 걸 수 있습니다. 그래서 Annex B를 씁니다. - fMP4는
moof+mdat의 반복 구조로 세그먼트 단위 독립성을 확보하고, HLS와 DASH가 같은 조각을 공유하게 만듭니다. - RTMP에서 HLS(
.ts)로 넘어갈 때 AVCC에서 Annex B로의 변환이 일어납니다. Start Code 교체와00 00 03이스케이프를 빼먹으면 디코더가 죽습니다. 가장 흔한 트러블슈팅 포인트입니다.