Skip to content

FLV, TS, fMP4: 컨테이너마다 달라지는 NAL 유닛 포장 방식 ​

같은 NAL 유닛인데 컨테이너가 바뀌면 왜 죽을까요. RTMP로는 멀쩡하던 스트림이 HLS에서 첫 프레임에 터지는 이유를 따라가 보겠습니다.

먼저 사건부터: 인제스트는 정상인데 배포에서 크래시가 난다 ​

실제 운영 중이던 라이브 채널에서 있었던 일입니다.

  • 인코더에서 서버로 올라오는 구간(RTMP): 플레이어로 확인하면 정상 재생됩니다.
  • 같은 스트림을 HLS로 패키징한 뒤: 첫 프레임에서 디코더가 크래시합니다.

비트스트림 자체는 복사한 거나 다름없는데 결과가 갈렸습니다. 차이는 하나뿐이었습니다. NAL 유닛을 감싸는 포장 방식이 달라졌습니다.

text
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-TS188바이트로 쪼개지며, 중간부터 동기화해야 합니다스트림 안에서 스스로 찾을 수 있는 Start Code (Annex B)
fMP4moof에 샘플 길이 표가 있습니다길이 prefix (AVCC)

핵심은 TS만 스트림 중간에서 자기 위치를 증명해야 한다는 점입니다. UDP로 방송을 쏘면 패킷이 뒤섞이고 손실도 생기는데, 이때 0x47로 시작하는 188바이트 경계를 훑어서 아무 데서나 동기화를 걸 수 있어야 합니다. AVCC처럼 "앞에 길이가 있다"는 약속은 한 바이트만 밀려도 무너집니다.

RTMP/FLV, SRT, MPEG-TS, fMP4/CMAF가 각각 어떤 계층을 거쳐 NAL 유닛을 감싸는지, 그리고 최종적으로 AVCC와 Annex B 중 어느 포장을 쓰는지 비교하는 다이어그램

RTMP: FLV 태그로 포장한다 ​

RTMP는 Flash Player 시대의 유산이지만, 여전히 **송출 구간(인코더에서 서버로)**에서 가장 많이 쓰입니다.

RTMP 패킷 안의 FLV 태그가 Audio Tag와 Video Tag로 나뉘고, Video Tag 안에서는 SPS/PPS가 담긴 AVC Decoder Config Record가 NAL 데이터보다 먼저 전송되는 구조

여기서 중요한 건 순서입니다. 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 byte1바이트 (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)**가 이걸 해결했습니다.

방식구조특징
기존 MP4moov ... 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 포장주 용도
RTMPTCPAVCC (FLV 태그)송출, 기여도 구간
SRTUDP + 재전송Annex B (TS)송출, 공용망 장거리
MPEG-TSUDP/TCPAnnex B방송, HLS(.ts)
fMP4 / CMAFHTTPAVCCHLS(.m4s), DASH(.m4s)

정리하면 규칙은 단순합니다.

text
스트림 중간에서 동기화가 필요하다  ->  Annex B (Start Code)
컨테이너가 길이를 이미 알고 있다   ->  AVCC (길이 prefix)

정리 ​

  • NAL 유닛 자체는 같고, 컨테이너가 바뀌면 경계 표시법만 달라집니다. 문제는 거의 항상 그 변환 지점에서 생깁니다.
  • RTMP는 연결 직후 AVC Decoder Config Record로 SPS/PPS를 먼저 보냅니다. 이게 유실되면 디코더는 초기화조차 못 합니다.
  • TS는 188바이트와 0x47 sync byte 덕분에 스트림 중간 어디서든 동기화를 걸 수 있습니다. 그래서 Annex B를 씁니다.
  • fMP4는 moof + mdat의 반복 구조로 세그먼트 단위 독립성을 확보하고, HLS와 DASH가 같은 조각을 공유하게 만듭니다.
  • RTMP에서 HLS(.ts)로 넘어갈 때 AVCC에서 Annex B로의 변환이 일어납니다. Start Code 교체와 00 00 03 이스케이프를 빼먹으면 디코더가 죽습니다. 가장 흔한 트러블슈팅 포인트입니다.

基于 VitePress 构建 · 部署于腾讯云 EdgeOne Pages