개발

영상 파일 공부

소혼 2026. 9. 26. 13:14

영상 파일을 공부하게 될 줄은 몰랐다.
AI 써서 공부하고 공부해서 내가 이해한 것들을 옮김.

=========================================

0. 영상 파일

영상 파일은 비디오 코덱, 오디오 코덱, 컨테이너(MP4, fMP4, MPEG-TS, WebM)로 구성

1. 컨테이너

구분 MP4 fMP4 MPEG-TS WebM
정식 계열 ISO BMFF ISO BMFF MPEG-2 TS Matroska 계열
파일 확장자 .mp4 보통 .mp4, .m4s .ts .webm
대표 코덱 H.264, HEVC, AAC 등 H.264, HEVC, AAC 등 H.264, HEVC, AAC, MPEG-2 등 VP8/VP9, AV1, Opus/Vorbis
구조 전체 파일 중심 여러 조각(fragment) 작은 패킷들의 연속 EBML 기반
스트리밍 가능하지만 전통적으로 파일 중심 매우 적합 매우 적합 적합
DASH 사용 가능 주력 가능 가능
HLS 가능 현대 HLS의 주요 방식 전통적인 방식 일반적이지 않음
브라우저 지원 매우 높음 매우 높음 재생 환경에 따라 다름 높음
랜덤 접근 좋음 fragment 단위 상대적으로 불편 좋음

컨테이너는 코덱 데이터를 샘플 단위로 담고, 각 샘플에 "크기, 위치, DTS, PTS, 키프레임 여부" 다섯 가지를 붙여 관리한다고 보면 됩니다. DTS는 디코더에 넣는 순서를, PTS는 보여주는 시각을 정하고, 둘의 차이는 B프레임 재정렬 때문에 생깁니다. 키프레임은 이 흐름에 들어갈 수 있는 입구입니다. MP4, fMP4, TS, WebM은 이 정보를 적는 위치와 형식만 다를 뿐, 담는 내용은 본질적으로 같습니다.

MP4

MP4는 박스(box, atom) 라는 단위로 구성. 핵심 박스는 세 가지.

  • ftyp: 파일 종류 선언
  • moov: 영상 전체의 메타데이터와 목차. 각 프레임이 파일의 몇 번째 바이트에 있고 재생 시점이 언제인지
  • mdat: 실제 인코딩된 영상·음성 데이터

moov에 전체 목차가 한꺼번에 들어 있어서 탐색(seek)이 빠르고 효율적입니다.
대신 인코딩이 끝나야 목차를 완성할 수 있기 때문에, 보통 moov가 파일 끝에 기록됩니다. 그 결과 다음과 같은 특징이 생깁니다.

  • 웹에서 바로 재생하려면 moov를 앞으로 옮겨야 합니다. ffmpeg의 -movflags +faststart 옵션이 이 작업을 합니다.
  • 녹화 중에 프로그램이 죽어 moov가 기록되지 못하면 파일 전체가 재생되지 않습니다.
  • 끝이 정해지지 않은 라이브 스트리밍에는 맞지 않습니다.

그래서 다운로드용 파일, VOD, 편집용 파일처럼 완성된 파일에 가장 잘 맞습니다.

fMP4 (Fragmented MP4)

fMP4는 MP4를 라이브와 스트리밍에 맞게 조각(fragment) 단위로 쪼갠 형태입니다. 구조는 다음과 같습니다.

  • 초기화 세그먼트: ftyp 다음에 moov가 옵니다. 여기에는 코덱 정보만 있고 프레임 목차는 없습니다.
  • 그 뒤로 moof 다음에 mdat이 오는 묶음이 반복됩니다. moof는 해당 조각만의 작은 목차입니다.

각 조각이 자기 목차를 들고 다니니까 전체 길이를 몰라도 계속 이어 붙일 수 있습니다. 중간에 끊겨도 이미 기록된 조각은 살아 있습니다. 이런 특성 때문에 현대 스트리밍의 표준이 되었습니다.

  • MPEG-DASH, 최신 HLS, CMAF(HLS와 DASH가 세그먼트를 공유하는 규격)에서 씁니다.
  • 브라우저의 MSE(Media Source Extensions)가 기본으로 받아들이는 형식이라 웹 플레이어와 궁합이 좋습니다.

fMP4 구조

MPEG-TS (Transport Stream)

MPEG-TS는 1990년대 디지털 방송을 위해 만들어졌습니다. 설계 전제는 "전파나 네트워크로 흘려보내는데, 수신기가 아무 때나 켜질 수 있고 데이터가 일부 손실될 수 있다"는 것입니다.

  • 모든 데이터를 188바이트 고정 크기 패킷으로 잘게 쪼갭니다.
  • 각 패킷에는 PID(어느 스트림 소속인지)가 붙습니다.
  • 프로그램 구성표(PAT, PMT)를 주기적으로 반복해서 보냅니다. 그래서 스트림 중간 어디서 받기 시작해도 곧 재생할 수 있습니다.
  • 패킷 일부가 깨져도 그 부분만 손상되고 나머지는 계속 재생됩니다.

단점은 오버헤드입니다. 패킷마다 헤더가 붙고 표를 반복해서 보내므로 MP4보다 용량이 조금 더 큽니다. 또 브라우저가 기본으로 재생하지 못합니다(Safari의 HLS 재생은 예외). 그래서 hls.js 같은 플레이어는 TS를 받아서 fMP4로 변환(transmux)한 뒤 MSE에 넣습니다.

주로 쓰이는 곳은 방송(DVB, ATSC, IPTV), 전통적인 HLS의 .ts 세그먼트, 그리고 SRT나 UDP 기반 실시간 전송입니다.

WebM

WebM은 Google이 만든 형식으로, Matroska(MKV)의 부분집합입니다. EBML이라는 구조를 씁니다. 가장 큰 특징은 로열티 없는 코덱만 허용한다는 점입니다.

  • 영상: VP8, VP9, AV1
  • 음성: Vorbis, Opus

H.264나 AAC는 WebM에 넣을 수 없습니다. 특허료 걱정이 없는 웹 영상을 목표로 만들어졌고, YouTube, 브라우저의 MediaRecorder(Chrome에서 녹화하면 기본으로 WebM이 나옴), WebRTC 녹화 등에서 많이 쓰입니다. 예전에는 Safari 지원이 약했지만 지금은 많이 나아졌습니다.

 

2. 샘플과 타임스탬프

타임스케일(timescale)

컨테이너는 시간을 초 단위 소수로 적지 않고, 정수 틱(tick) 으로 적습니다. 1초를 몇 틱으로 나눌지 정한 값이 타임스케일입니다.

  • MP4는 트랙마다 mdhd 박스에 타임스케일이 있습니다. 비디오는 90000이나 프레임레이트의 배수(예: 30fps면 15360, 30000 등)를 많이 쓰고, 오디오는 보통 샘플레이트(48000 등)를 그대로 씁니다.
  • MPEG-TS는 90kHz로 고정입니다. PES 패킷 헤더에 PTS/DTS가 33비트 정수로 들어갑니다.
  • WebM은 기본 1ms 단위(TimecodeScale = 1,000,000ns)입니다.

실제 시각은 틱 값 ÷ 타임스케일로 계산합니다. 타임스케일 90000에서 PTS가 180000이면 2초입니다. 정수를 쓰는 이유는 부동소수점 오차가 쌓이지 않게 하려는 것입니다. 29.97fps처럼 딱 떨어지지 않는 프레임레이트도 타임스케일 30000, 프레임 길이 1001로 정확하게 표현할 수 있습니다.

샘플(sample)

샘플은 컨테이너 입장에서 타임스탬프 하나가 붙는 최소 데이터 단위입니다. 컨테이너는 샘플 내부를 해석하지 않고, "이 바이트 덩어리는 크기가 몇이고, 언제 디코딩하고, 언제 보여주고, 키프레임인가"만 관리합니다.

코덱별로 보면 다음과 같습니다.

  • H.264/H.265: 프레임 1개가 샘플 1개입니다. 프레임 하나는 여러 NAL 유닛(슬라이스, SEI 등)으로 이루어질 수 있는데, 이것들을 묶어 한 샘플로 넣습니다. MP4에서는 NAL 앞에 시작 코드(00 00 00 01) 대신 길이 필드를 붙이는 방식(AVCC)을 씁니다. TS는 시작 코드 방식(Annex B)을 씁니다. 같은 H.264라도 MP4와 TS 사이를 옮길 때 이 변환이 필요한 이유가 여기 있습니다.
  • AAC: AAC 프레임 1개(보통 1024 오디오 샘플, 48kHz 기준 약 21.3ms)가 컨테이너 샘플 1개입니다. 여기서 "오디오 샘플"(PCM 값 하나)과 "컨테이너 샘플"(AAC 프레임 하나)은 다른 개념이라 헷갈리기 쉽습니다.
  • 자막: 자막 한 줄(표시 구간 하나)이 샘플 하나입니다.

MP4에서는 샘플 정보가 moov 안의 샘플 테이블(stbl)에 흩어져 기록됩니다.

박스담는 정보
stsz 각 샘플의 크기(바이트)
stco / co64 청크(샘플 묶음)의 파일 내 위치
stsc 어느 청크에 샘플이 몇 개 들어 있는지
stts 각 샘플의 디코딩 길이 → DTS 계산
ctts DTS와 PTS의 차이
stss 키프레임인 샘플 번호 목록

플레이어는 이 표들을 조합해서 "N번 샘플은 파일 몇 바이트 위치에 몇 바이트 크기로 있고, DTS는 얼마, PTS는 얼마"를 계산합니다.

fMP4에서는 이 정보가 조각마다 moof 안에 들어갑니다. tfdt에는 조각의 시작 DTS가, trun에는 샘플별 길이, 크기, 플래그(키프레임 여부), PTS 오프셋이 들어갑니다.

DTS와 PTS

왜 두 개가 필요한가

B프레임은 앞 프레임과 뒤 프레임을 모두 참조합니다. 그래서 뒤에 보여줄 프레임을 먼저 디코딩해 둬야 B프레임을 풀 수 있습니다. 표시 순서가 I B B P라면 실제 디코딩 순서는 I P B B가 됩니다.

컨테이너 안에서 샘플은 디코딩 순서대로 저장됩니다. 디코더에 들어가는 순서가 그래야 하기 때문입니다. 각 샘플에는 두 시각이 붙습니다.

  • DTS(Decoding Time Stamp): 디코더에 넣을 시각. 저장 순서대로 단조 증가합니다.
  • PTS(Presentation Time Stamp): 화면에 표시할 시각. 저장 순서 기준으로는 오르락내리락합니다.

엄밀히는 DTS도 "순서"가 아니라 "시각"이지만, 실무에서는 사실상 디코딩 순서를 표현하는 역할을 합니다.

구체적인 예

30fps, 타임스케일 30(프레임 1개 = 1틱)으로 단순화해 보겠습니다. 표시 순서는 I0 B1 B2 P3입니다.

저장(디코딩) 순서프레임DTSPTSctts 오프셋 (PTS−DTS)
1 I0 0 1 1
2 P3 1 4 3
3 B1 2 2 0
4 B2 3 3 0

PTS가 전부 1씩 밀려 있는 점에 주목하세요. 원래 표시 시각대로 PTS를 0, 3, 1, 2로 적으면 B1은 DTS 2에 디코딩되는데 PTS 1에 보여줘야 합니다. 디코딩도 안 된 프레임을 먼저 보여줄 수는 없습니다. 그래서 PTS ≥ DTS 조건을 지키도록 표시 시각 전체를 뒤로 밀어 둡니다. 이 밀린 양을 디코더 지연(reorder delay)이라고 하고, B프레임을 연속으로 많이 쓸수록 커집니다.

MP4에서의 기록 방식

  • stts에는 각 샘플의 길이만 적습니다. DTS는 누적합으로 계산됩니다(0, 1, 2, 3…). 길이가 같은 샘플이 연속되면 "길이 1인 샘플 4개"처럼 압축해서 적습니다.
  • ctts에는 PTS−DTS 오프셋을 적습니다(1, 3, 0, 0). B프레임이 없으면 PTS = DTS이므로 ctts 박스 자체가 없습니다.

밀린 시작 시각의 처리

위 예에서 첫 프레임의 PTS가 0이 아니라 1이므로, 그대로 두면 영상이 1프레임 늦게 시작하고 오디오와 어긋납니다. 해결 방법은 두 가지입니다.

  • 편집 목록(elst): "이 트랙은 미디어 시각 1부터 재생하라"고 적어서 앞부분을 잘라냅니다. ffmpeg가 만드는 MP4의 대부분이 이 방식입니다.
  • 음수 오프셋: ctts 버전 1은 음수를 허용합니다. 오프셋을 −1씩 조정해 첫 PTS를 0으로 맞출 수 있습니다. fMP4/CMAF에서 자주 씁니다.

플레이어가 elst를 무시하면 오디오와 비디오가 미세하게 어긋나는 문제가 생기는데, 실무에서 싱크 버그의 흔한 원인 중 하나입니다.

오디오는 보통 단순하다

AAC 같은 오디오 코덱은 프레임 재정렬이 없으므로 PTS = DTS입니다. 대신 인코더 지연(priming)이라는 비슷한 문제가 있습니다. AAC 인코더는 앞부분에 약 1024~2112 오디오 샘플의 무음 구간을 만들어 냅니다. 이것 역시 elst로 잘라내야 정확한 싱크와 끊김 없는 재생(gapless)이 됩니다.

키프레임(sync sample)

키프레임은 다른 프레임을 참조하지 않고 단독으로 디코딩할 수 있는 프레임.
이 프레임 이후의 프레임들이 이 프레임 이전 프레임을 참조하지 않아야 진짜 "여기서부터 시작 가능한 지점"이 됩니다. 이 두 번째 조건이 중요합니다.

  • IDR 프레임(H.264): 디코더의 참조 버퍼를 비웁니다. 이후 프레임은 절대 이전 프레임을 참조하지 않으므로 완전한 시작점입니다. Closed GOP라고 합니다.
  • IDR이 아닌 I 프레임: 그 자체는 단독으로 풀 수 있지만, 뒤따르는 B프레임이 이 I 앞의 프레임을 참조할 수 있습니다. 여기서 시작하면 첫 몇 개 B프레임이 깨집니다. Open GOP라고 합니다. H.265의 CRA 프레임이 대표적이고, 이런 경우 앞의 깨지는 프레임을 버리는 규칙(RASL 프레임 폐기)이 코덱에 정의돼 있습니다.

컨테이너에서의 표시

  • MP4: stss에 키프레임 샘플 번호 목록이 있습니다. stss가 아예 없으면 "모든 샘플이 키프레임"이라는 뜻이고, 오디오 트랙이 보통 이렇습니다.
  • fMP4: trun의 샘플 플래그에 sample_is_non_sync_sample 비트로 표시합니다.
  • MPEG-TS: 적응 필드(adaptation field)의 random_access_indicator 비트로 표시합니다. 그러나 이 비트가 누락되는 경우가 많아서, 실무에서는 NAL 유닛을 직접 읽어 IDR인지 확인하기도 합니다.
  • WebM: SimpleBlock의 키프레임 플래그와 Cues(탐색용 색인)로 표시합니다.

탐색(seek)이 실제로 동작하는 방식

사용자가 12.5초로 이동하면 플레이어는 이렇게 합니다.

  1. 12.5초에 해당하는 샘플을 찾습니다.
  2. 키프레임 목록에서 그 샘플 이전에 있는 가장 가까운 키프레임을 찾습니다. 예를 들어 10초에 있다고 합시다.
  3. 10초 키프레임부터 디코딩을 시작합니다.
  4. 여기서 두 가지 선택이 있습니다. 빠른 탐색은 그냥 10초부터 보여줍니다. 정확한 탐색은 10~12.5초 구간을 디코딩만 하고 화면에는 버린 뒤 12.5초부터 보여줍니다.

키프레임 간격(GOP 길이)이 길면 압축 효율은 좋아지지만, 탐색이 부정확하거나 느려집니다. 그래서 목적에 따라 간격을 다르게 잡습니다.

  • 스트리밍(HLS/DASH): 세그먼트가 반드시 키프레임으로 시작해야 합니다. 그래야 세그먼트 단위로 화질을 바꾸거나 중간부터 재생할 수 있습니다. 보통 2초 간격 고정 GOP를 씁니다.
  • 저지연 라이브: 1초 이하로 짧게 잡습니다.
  • 보관용 파일: 5~10초로 길게 잡기도 합니다.

ffmpeg에서 -ss로 자를 때 -c copy(재인코딩 없음)를 쓰면 시작점이 키프레임에 맞춰 당겨지거나 앞부분이 깨지는 현상도 같은 원리입니다. 키프레임이 아닌 곳에서는 스트림을 시작할 수 없기 때문입니다.

3. MP4 & fMP4

MP4의 핵심, 박스: size(4byte) + type(4byte) + box contents

예제: 00 00 00 1C 66 74 79 70 ...
00 00 00 1C <- size: 28
66 74 79 70 <- type: ftyp
박스 내용 이번 진단에서 본 것
ftyp 파일 종류 맨 앞에 있어야 함
mdat 실제 데이터(모든 샘플의 바이트) 2GB 영상이면 대부분이 여기
moov 색인(트랙별 샘플 표) MediaMuxer는 파일 끝에 씀. 로컬 파일은 문제 없음
mdhd 트랙 시간 단위(timescale)·길이 비디오 90000, 오디오 48000 등
stts 샘플별 길이(시간) AAC는 샘플이 1024여야 정상.
stsz 샘플별 크기·개수 샘플 수 = 길이×샘플레이트÷1024여야 함
stco/co64 청크(샘플 묶음)의 파일 위치 비디오·오디오가 섞여 있는지(인터리브)
stss 키프레임 목록 첫 항목이 1이어야 처음부터 재생 가능
ctts 표시·디코딩 순서 차이 B프레임이 있는 H.264에만
elst 트랙 시작 보정(편집 목록) 비디오 mediaTime 9040(0.1초)은 B프레임용 정상값
esds AAC 설정(AudioSpecificConfig) 샘플은 "헤더 없는 AAC 프레임"이라는 약속

3.1 ftyp — File Type Box

가장 먼저 자주 등장하는 것이 ftyp이다.

ftyp
├── major_brand
├── minor_version
└── compatible_brands[]
 

예를 들어:

ftyp
  major_brand = isom
  minor_version = 512
  compatible_brands =
      isom
      iso2
      avc1
      mp41
 

이것은 코덱 정보가 아니다.

"이 파일이 어떤 ISO Base Media File Format 계열과 호환되는가"를 나타내는 정보다.

그래서:

ftyp = H.264
 

가 아니다.

H.264인지 여부는 뒤쪽의 stsd 등을 봐야 한다.


3.2 moov — MP4의 핵심

moov가 사실 MP4에서 가장 중요하다.

moov
├── mvhd
├── trak
├── trak
└── ...
 

moov는 쉽게 말하면:

"이 영상 파일을 어떻게 해석해야 하는지 알려주는 인덱스/메타데이터 영역"

이다.

예를 들어 영상이

1920 × 1080
30 fps
H.264
10분
 

이라면 이런 정보를 알아야 실제 mdat를 해석할 수 있다.


3.3 mvhd — Movie Header

moov
└── mvhd
 

전체 movie에 대한 정보가 들어간다.

대표적으로:

  • movie timescale
  • duration
  • creation/modification time
  • playback rate
  • volume
  • transformation matrix

등이 있다.

여기서 timescale이 중요하다.

예를 들어:

timescale = 1000
duration = 600000
 

이면

600000 / 1000 = 600초
 

즉 10분이다.

MP4에서는 시간을 단순히 "초"로 저장하지 않고 정수 단위의 시간 tick으로 표현하는 경우가 많다.


3.4 trak — Track

그 다음이 trak이다.

moov
├── trak  ← video
└── trak  ← audio
 

즉 하나의 MP4 안에 여러 track이 있을 수 있다.

예를 들어:

trak #1
 └─ video
     └─ H.264

trak #2
 └─ audio
     └─ AAC

trak #3
 └─ subtitle
 

이런 구조가 가능하다.


3.5 tkhd — Track Header

trak
└── tkhd
 

각 track 자체의 정보가 들어간다.

예를 들어 video track이라면:

track ID = 1
width = 1920
height = 1080
 

등이 들어간다.

여기서 중요한 점은:

MP4의 width/height가 한 군데만 있는 것은 아니다.

여러 레벨에 관련 정보가 존재할 수 있다.


3.6 mdia — Media

trak
└── mdia
    ├── mdhd
    ├── hdlr
    └── minf
 

여기부터 실제 media 자체를 설명한다.

mdhd

Media의 시간 정보.

mdhd
├── timescale
└── duration
 

hdlr

이 track이 무엇인지 알려준다.

예:

hdlr
  handler_type = vide
 

이면 video.

hdlr
  handler_type = soun
 

이면 audio.

즉:

vide → video
soun → sound
 

이다.


3.7 minf — Media Information

mdia
└── minf
    └── stbl
 

여기에는 해당 media track을 실제로 처리하기 위한 정보가 들어간다.

Video라면 vmhd, Audio라면 smhd 등이 있다.

하지만 실제로 중요한 것은 그 아래의 **stbl**이다.


3.8 stbl — Sample Table

여기가 MP4를 이해하는 데 가장 중요한 부분 중 하나다.

stbl
├── stsd
├── stts
├── ctts
├── stsc
├── stsz
├── stco / co64
└── stss
 

이것들을 조합하면:

"mdat 안에서 어떤 데이터가 어느 위치에 있고, 얼마나 크고, 언제 재생되고, 어떤 순서로 디코딩되는가?"

를 알 수 있다.


3.9 stsd — Sample Description

stsd
 

는 이 track의 데이터가 어떤 형식인지 알려준다.

예를 들어 H.264라면:

stsd
└── avc1
    └── avcC
 

HEVC라면:

stsd
└── hvc1 / hev1
    └── hvcC
 

AAC라면:

stsd
└── mp4a
    └── esds
 

같은 식이다.

그래서 MP4를 보고:

avc1
 

이 발견되면

"아, 이 track은 H.264구나."

라고 알 수 있다.


3.10 avcC — H.264 설정 정보

예를 들어:

stsd
└── avc1
    └── avcC
 

가 있다면 avcC 안에 H.264 decoder configuration이 들어간다.

대표적으로:

  • SPS
  • PPS
  • profile
  • level
  • NAL length size

등이다.

즉 디코더가 H.264를 디코딩하기 시작할 때 필요한 정보다.


3.11 stts — 시간 정보

이것도 상당히 중요하다.

stts
 

는 Decoding Time에 대한 정보를 담는다.

예를 들어:

sample 1 → 0
sample 2 → 33
sample 3 → 66
sample 4 → 99
 

처럼 sample이 언제 디코딩되어야 하는지 알 수 있다.

30fps라면 대략:

timescale = 1000

sample duration ≈ 33
 

같은 식으로 표현될 수 있다.


3.12 ctts — Composition Time

영상에서는 DTS와 PTS가 다를 수 있다.

특히 B-frame 때문이다.

예를 들어:

Decode order:

I → B → B → P
 

인데 화면에 보여주는 순서는:

I → B → B → P
 

처럼 보이지만 내부적으로 decode 순서와 presentation 순서가 달라질 수 있다.

그래서:

stts = decoding timestamp
ctts = composition/presentation offset
 

라고 생각하면 된다.

Chromium/WebKit의 미디어 파이프라인을 볼 때 이 부분이 상당히 중요하다.


3.13 stsz — Sample Size

각 sample의 크기를 알려준다.

예를 들어:

stsz

sample 1 = 12000 bytes
sample 2 = 3500 bytes
sample 3 = 2800 bytes
sample 4 = 14000 bytes
...
 

이런 식이다.

영상에서는 frame 하나가 하나의 sample이 되는 경우가 많다.


3.14 stco / co64 — 데이터 위치

그리고 실제 데이터가 mdat의 어디에 있는지 알아야 한다.

그 역할을 하는 것이:

stco
 

또는

co64
 

다.

예를 들어:

sample 1 → offset 100000
sample 2 → offset 112000
sample 3 → offset 115500
 

이런 정보를 갖는다.

그러면 parser가:

stco + stsz
 

를 이용해서

"sample 2는 파일의 이 위치에서 이 크기만큼 읽으면 되는구나."

라고 알 수 있다.


3.15 mdat — 진짜 영상 데이터

그리고 드디어:

mdat
 

이다.

여기에 실제 encoded media data가 들어간다.

예를 들어 H.264라면:

mdat
│
├── H.264 sample 1
├── H.264 sample 2
├── H.264 sample 3
├── H.264 sample 4
└── ...
 

중요한 것은:

mdat 자체에는 "이게 몇 번째 frame인지" 같은 고수준 정보가 거의 없다.

그 정보를 moov의 sample table이 설명한다.

즉 둘을 합쳐야 한다.

              moov
                │
       ┌────────┴─────────┐
       │                   │
 sample table          codec config
       │
       ↓
"sample #23은
 offset X,
 size Y,
 DTS Z"
       │
       ↓
      mdat
       │
       ↓
   실제 H.264 데이터
 

3.16 그래서 MP4 parser의 핵심은 이런 관계다

개념적으로 보면:

                   MP4
                    │
           ┌────────┴────────┐
           ↓                 ↓
         moov              mdat
           │                 │
       metadata          encoded data
           │
           ↓
        trak
           │
         mdia
           │
         minf
           │
         stbl
           │
     ┌─────┼─────┬─────┬─────┐
     ↓     ↓     ↓     ↓     ↓
    stsd  stts  stsz  stco  ctts
     │
     ↓
   avc1
     │
     ↓
    avcC
 

이렇게 연결된다.

3.17 fMP4가 되면 구조가 달라진다

여기서 앞에서 이야기한 fMP4가 등장한다.

일반 MP4:

ftyp
moov
mdat
 

fMP4:

ftyp
moov

moof
mdat

moof
mdat

moof
mdat
 

가 된다.

moof는 Movie Fragment이고,

mdat는 그 fragment의 실제 데이터다.

대략:

moof
├── mfhd
└── traf
    ├── tfhd
    ├── tfdt
    └── trun

mdat
└── encoded samples
 

여기서 특히:

tfhd
tfdt
trun
 

이 중요하다.

trun에는 fragment 안의 sample들에 대한:

  • size
  • duration
  • flags
  • composition time offset

등의 정보가 들어갈 수 있다.

즉 일반 MP4에서 stbl이 하던 역할의 일부를 fragment별로 moof 안에서 나눠 가지고 있는 것이라고 이해하면 된다.


3.18 왜 fMP4가 스트리밍에 좋은가?

일반 MP4는:

ftyp
moov ────────────────┐
                     │
mdat                 │
█████████████████████│
 

처럼 전체 파일에 대한 index가 필요하다.

반면 fMP4는:

ftyp
moov

moof + mdat   ← 0~2초
moof + mdat   ← 2~4초
moof + mdat   ← 4~6초
moof + mdat   ← 6~8초
 

처럼 각 fragment가 독립적으로 필요한 정보를 가지고 있다.

그래서 HTTP로:

Range request
       ↓
fragment 23
       ↓
moof + mdat
       ↓
decode
 

같은 방식으로 다루기가 좋다.


3.19 마지막으로 가장 중요한 그림

MP4를 "코덱 데이터가 들어 있는 상자"라고만 생각하면 조금 부족하고, 다음처럼 생각하면 정확하다.

                    MP4
                     │
        ┌────────────┴────────────┐
        │                         │
    설명/인덱스                  데이터
        │                         │
       moov                      mdat
        │                         │
   ┌────┴────┐                    │
   │         │                    │
 video      audio                 │
   │         │                    │
  trak      trak                  │
   │                              │
  mdia                            │
   │                              │
  minf                            │
   │                              │
  stbl                            │
   │                              │
 ┌─┼──────┬──────┬──────┐         │
 │ │      │      │      │         │
stsd stts  stsz   stco   ctts      │
 │                                │
 ↓                                ↓
H.264                         실제 NAL/sample
config                         bytes
 

**한마디로 moov가 "설명서/색인"이고 mdat가 "실제 영상 창고"**라고 생각하면 된다.

 

4. 스트리밍

웹에서 영상을 틀어 주는 방식은 크게 네 가지

방식 페이지가 쓰는 주소 실제 데이터
progressive https://…/video.mp4 파일 하나
HLS …/index.m3u8 재생목록 + 짧은 세그먼트 수백~수천 개
DASH …/manifest.mpd XML 매니페스트 + 세그먼트
MSE(blob) blob:https://… JS가 받은 데이터를 브라우저에 밀어 넣음

4.1 Progressive

가장 단순하다.

<video src="https://example.com/video.mp4">
 

실제 데이터는 그냥 하나의 MP4 파일

video.mp4
┌────────────────────────────────────────────┐
│ ftyp │ moov │ mdat                        │
│      │      │ H264 + AAC                  │
└────────────────────────────────────────────┘
 

여기서 "progressive"라고 하는 이유는 파일 전체를 다운로드할 때까지 기다리지 않고 받은 데이터부터 재생할 수 있기 때문이다.

예를 들어:

다운로드
████████████████████████████████
       ↑
       현재 다운로드 위치

재생
████████
       ↑
       현재 재생 위치
 

다운로드와 재생이 동시에 진행된다.

중요한 점

Progressive라고 해서 반드시 "처음부터 끝까지 전부 다운로드한다"는 것은 아니다.
브라우저가 HTTP Range Request를 사용하면:

Range: bytes=10000000-20000000
 

처럼 MP4의 특정 부분을 요청할 수도 있다.

따라서:

Progressive = 단일 미디어 파일을 HTTP를 통해 재생

이라고 이해하는 게 정확.

4.2 HLS — "파일 하나"가 아니라 "재생목록을 따라간다"

HLS에서는 <video>가 MP4를 직접 받는 것이 아니다.

처음에 playlist인, m3u8 을 받는다.

https://example.com/video/index.m3u8
 

예를 들어:

index.m3u8

1080p.m3u8
720p.m3u8
480p.m3u8
 

같은 정보를 가지고 있을 수 있다.

즉:

Browser
   │
   │ GET index.m3u8
   ↓
Server
   │
   │ playlist
   ↓
Browser
 

그리고 playlist를 보고 실제 영상을 찾는다.

예를 들어 720p playlist가:

720p.m3u8
 

이고 내부적으로:

segment001.ts
segment002.ts
segment003.ts
segment004.ts
...
 

즉 m3u8 자체에는 보통 영상 데이터가 없다.

m3u8은 사실상 "다음에 이 URL들을 받아서 재생해." 라는 목록/설명서다.

예를 들어 2시간짜리 영상이고 segment 하나가 6초라면:

7200 / 6 = 1200개
 

정도가 된다.

그래서:

video
│
├── segment0001
├── segment0002
├── segment0003
├── ...
└── segment1200
 

이런 구조가 된다.

그리고 여기서 엄청 중요한 기능이 하나 생긴다.

화질을 바꿀 수 있다.

서버가:

1080p
 ├─ 001
 ├─ 002
 ├─ 003
 ...

720p
 ├─ 001
 ├─ 002
 ├─ 003
 ...

480p
 ├─ 001
 ├─ 002
 ├─ 003
 ...
 

를 갖고 있다면 플레이어가:

1080p segment 1
1080p segment 2
720p  segment 3
720p  segment 4
1080p segment 5
 

처럼 선택할 수 있다.

이것이 Adaptive Bitrate Streaming (ABR)이다.

4.3 DASH도 거의 같은 철학이다

DASH에서는 시작 주소가:

https://example.com/manifest.mpd
 

HLS의 m3u8에 대응하는 것이 DASH의 mpd다.

HLS              DASH
────              ────
m3u8              mpd
playlist          manifest
segment           segment
 

DASH의 manifest는 XML이다.

개념적으로:

<MPD>
  <Period>
    <AdaptationSet>
      <Representation bandwidth="800000">
        ...
      </Representation>

      <Representation bandwidth="4000000">
        ...
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>
 

이런 정보를 가지고 있다.

즉:

Browser / Player
       │
       │ GET manifest.mpd
       ↓
     MPD
       │
       ├── 1080p
       ├── 720p
       └── 480p
             │
             ↓
        각 segment URL
 

이라는 구조다.

4.4 HLS와 DASH의 본질적인 차이

둘의 핵심 구조는 사실 굉장히 비슷하다.

                 Streaming
                     │
          ┌──────────┴──────────┐
          ↓                     ↓
         HLS                   DASH
          ↓                     ↓
       m3u8                    mpd
          ↓                     ↓
      segments               segments
          ↓                     ↓
         ABR                   ABR
 

차이는 manifest 형식과 프로토콜/생태계의 역사적 차이가 크다.

그리고 컨테이너도 과거와 현재가 조금 다르다.

과거 HLS:

m3u8
 ↓
.ts
 

현대 HLS:

m3u8
 ↓
fMP4/CMAF
 

DASH:

mpd
 ↓
fMP4/CMAF
 

가 흔하다.

그래서 요즘은:

HLS와 DASH가 서로 다른 영상 포맷이라기보다, 같은 fMP4/CMAF 미디어를 서로 다른 manifest/스트리밍 규칙으로 전달할 수 있다

고 이해하면 좋다.


4.5 MSE가 완전히 다른 이유

네 표에서 가장 헷갈릴 수 있는 부분이 이거다.

MSE(blob)
blob:https://…
JS가 받은 데이터를 브라우저에 밀어 넣음
 

여기서는 blob URL이 실제 서버의 영상 URL이 아니다.

예를 들어 페이지에:

<video src="blob:https://example.com/abc-123">
 

가 있다고 하자.

이걸 보고:

"abc-123이라는 파일이 서버에 있나?"

라고 생각하면 안 된다.
MSE에서는 JavaScript가 중간에 있다

구조가 다음과 같이 된다.

Server
   ↑
   │ HTTP
   │
JavaScript
   │
   │ appendBuffer()
   ↓
MediaSource
   ↓
SourceBuffer
   ↓
<video>
 

예를 들어 JS가:

const mediaSource = new MediaSource();

video.src = URL.createObjectURL(mediaSource);
 

하면 브라우저가:

blob:https://example.com/...
 

같은 URL을 만든다.

이것은 MediaSource 객체를 <video>에 연결하기 위한 브라우저 내부적인 URL이다.

그리고 JS가 직접 데이터를 가져온다

예를 들어 다음과 같이 한다.

fetch("/video/segment001.m4s")
 

서버가:

moof + mdat
 

를 반환하면,  JS가 다음과 같이 한다.

sourceBuffer.appendBuffer(data);
 

그래서 다음과 같이 된다.

segment001.m4s
        ↓
   ArrayBuffer
        ↓
 appendBuffer()
        ↓
 SourceBuffer
        ↓
     <video>
 

즉 브라우저에게 데이터를 공급하는 주체가 JavaScript다.

5. HLS

5.1 HLS 전체 구조

HLS는 크게 Master Playlist → Media Playlist → Segment 3단계로 생각하면 된다.

                        HLS
                         │
                 master.m3u8
                         │
            ┌────────────┼────────────┐
            ↓            ↓            ↓
         1080p          720p         480p
            │            │            │
      media.m3u8    media.m3u8    media.m3u8
            │            │            │
       ┌────┼────┐   ┌───┼────┐   ┌───┼────┐
       ↓    ↓    ↓   ↓   ↓    ↓   ↓   ↓    ↓
      seg  seg  seg  seg seg  seg  seg seg  seg
 

그리고 오디오가 별도로 있다면:

                    master.m3u8
                         │
             ┌───────────┴───────────┐
             ↓                       ↓
          VIDEO                    AUDIO
             ↓                       ↓
         1080p.m3u8              audio.m3u8
             ↓                       ↓
       video segments            audio segments
 

이제 각각을 보자.


5.2 Master Playlist

처음 요청하는 것이 보통 이것이다.

https://example.com/hls/master.m3u8
 

내용은 대략:

#EXTM3U

#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/index.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
720p/index.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=854x480
480p/index.m3u8
 

여기서 중요한 것은 영상 데이터가 없다는 것이다.

Master Playlist는:

"이 영상에는 이런 버전들이 있습니다."

라고 알려주는 메뉴판이다.


5.2.1 #EXT-X-STREAM-INF

이 줄이 variant를 정의한다.

예를 들어:

#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/index.m3u8
 

의 의미는 다음과 같다.

variant
 ├── 예상 bitrate: 5 Mbps
 ├── 해상도: 1920×1080
 └── media playlist:
       1080p/index.m3u8
 

여기서 variant라는 말은 "동일한 콘텐츠의 다른 품질 버전"이라는 뜻이다.

따라서:

1080p
720p
480p
 

가 각각 하나의 variant다.


5.2.2 BANDWIDTH

예:

#EXT-X-STREAM-INF:BANDWIDTH=5000000
 

이것은 단순히:

"현재 파일이 5 Mbps다."

라는 의미라기보다 해당 variant를 재생하는 데 필요한 평균적인/최대 네트워크 대역폭 정보를 제공하는 값으로 이해하는 게 좋다.

플레이어가:

현재 네트워크 ≈ 3 Mbps
 

라면:

1080p = 5 Mbps
720p  = 2.5 Mbps
480p  = 1 Mbps
 

중에서 720p나 480p를 선택할 수 있다.

이것이 ABR의 출발점이다.


5.2.3 Master Playlist에는 오디오도 등장할 수 있다

여기가 조금 복잡해진다.

단순한 HLS라면:

1080p.m3u8 → video + audio
 

처럼 한 segment 안에 영상과 음성이 같이 들어갈 수 있다.

하지만 오디오를 별도로 분리할 수도 있다.

예를 들어:

Master
│
├── 1080p video
├── 720p video
├── 480p video
│
└── audio
      ├── Korean
      ├── English
      └── Japanese
 

이런 구조다.

Master Playlist에 이런 것이 나올 수 있다.

#EXT-X-MEDIA:TYPE=AUDIO,
GROUP-ID="audio",
NAME="Korean",
LANGUAGE="ko",
DEFAULT=YES,
URI="audio/ko.m3u8"
 

그리고 video variant:

#EXT-X-STREAM-INF:
BANDWIDTH=5000000,
RESOLUTION=1920x1080,
AUDIO="audio"

video/1080p.m3u8
 

이것은 다음과 같은 뜻이다.

"1080p 영상은 audio라는 오디오 그룹을 사용하고, 그 그룹의 기본 오디오는 Korean이다."

왜 오디오를 분리할까? 대표적인 이유가 언어 선택이다.

             VIDEO
               │
       ┌───────┼───────┐
       ↓       ↓       ↓
     1080p    720p    480p

             AUDIO
               │
       ┌───────┼───────┐
       ↓       ↓       ↓
     Korean  English  Japanese
 

영상은 하나인데:

한국어 음성
영어 음성
일본어 음성
 

을 선택할 수 있다.

또 다른 이유는 영상과 음성의 bitrate를 독립적으로 조절하기 위해서다.


5.3 Media Playlist

Master가:

1080p/index.m3u8
 

을 가리켰다고 가정해 보자.

#EXTM3U
#EXT-X-TARGETDURATION:6
#EXTINF:5.96,
segment001.m4s

#EXTINF:6.02,
segment002.m4s

#EXTINF:5.98,
segment003.m4s

#EXT-X-ENDLIST
 

이것이 Media Playlist다.

Media Playlist에는 실제 segment 목록이 들어 있다.


5.3.1 #EXTINF

예:

#EXTINF:5.96,
segment001.m4s
 

의 의미:

segment001.m4s
     ↑
약 5.96초의 미디어
 

이다.

즉:

segment001.m4s → 0~5.96초
segment002.m4s → 5.96~11.98초
segment003.m4s → 11.98~17.96초
 

처럼 플레이어가 시간축을 구성할 수 있다.


5.3.2 Segment 하나가 꼭 정확히 6초일 필요는 없다

중요한 점이다.

#EXTINF:6.00
#EXTINF:5.96
#EXTINF:6.03
#EXTINF:5.97
 

처럼 약간씩 다를 수 있다.

#EXTINF는 실제 segment duration을 설명하는 정보다.


5.3.3 #EXT-X-ENDLIST

이것도 매우 중요하다.

#EXT-X-ENDLIST
 

가 있다면:

"이 playlist는 더 이상 추가 segment가 없다."

라는 의미다.

그래서 일반적으로 VOD에서는:

segment1
segment2
segment3
...
segment100

#EXT-X-ENDLIST
 

가 된다.

반면 Live HLS는:

segment98
segment99
segment100
 

까지만 있고 ENDLIST가 없다.

조금 뒤 다시 playlist를 요청하면:

segment99
segment100
segment101
 

처럼 바뀐다.

Live HLS는 playlist 자체가 움직인다
이것이 VOD와 Live의 큰 차이다.

VOD

playlist
 ├─ 1
 ├─ 2
 ├─ 3
 ├─ ...
 └─ 100
     ENDLIST
 

Live

시간 T:

playlist
 ├─ 98
 ├─ 99
 └─ 100
 

몇 초 후:

playlist
 ├─ 99
 ├─ 100
 └─ 101
 

다시:

playlist
 ├─ 100
 ├─ 101
 └─ 102
 

즉 플레이어가 playlist를 주기적으로 다시 받아서 새로운 segment를 발견한다.

5.4 #EXT-X-MAP

이제 fMP4로 들어간다.

전통적인 HLS는 다음과 같다.

m3u8
 ↓
segment.ts
segment.ts
segment.ts
 

그런데 fMP4를 쓰면 다음과 같이 된다.

#EXT-X-MAP:URI="init.mp4"

segment001.m4s
segment002.m4s
segment003.m4s
 

#EXT-X-MAP은 다음 의미이다.

"이 segment들을 해석하기 위한 initialization segment는 여기 있다."

앞에서 MP4 구조를 봤기 때문에 이제 쉽게 이해할 수 있다.

일반 MP4:

ftyp
moov
mdat
 

fMP4에서는 다음과 같다.

init segment
    ↓
ftyp + moov

media segments
    ↓
moof + mdat
moof + mdat
moof + mdat
 

즉:

init.mp4
   ↓
"이 미디어는 H.264이고,
 timescale은 이것이고,
 track은 이것이고,
 decoder configuration은 이것이다."
 

를 알려준다.

그리고 실제 segment는 다음과 같다.

segment1.m4s
    ↓
moof + mdat
    ↓
실제 sample
 

따라서 #EXT-X-MAP이 보이면 거의 확실하게:

HLS
 ↓
fMP4
 ↓
CMAF 계열
 

을 의심할 수 있다.

반대로:

#EXTINF:6,
segment001.ts
 

처럼 .ts를 사용하면 전통적인 MPEG-TS 기반 HLS일 가능성이 높다.

다만 확장자만으로 100% 판단하는 것보다는 실제 파일의 MIME/type과 box/TS 구조를 보는 것이 확실하다.


5.5 이제 암호화

예를 들어:

#EXT-X-KEY:METHOD=AES-128,
URI="https://example.com/key"
 

라면:

"이후 segment는 AES-128 방식으로 암호화되어 있으니 이 key를 가져와 복호화해서 사용해라."

라는 의미다.

구조를 보면:

playlist
    │
    ├── #EXT-X-KEY
    │       ↓
    │     key URL
    │
    ↓
encrypted segment
    │
    ↓
   AES-128
    │
    ↓
clear media
 

다만, AES-128 HLS와 DRM은 구별해야 한다

HLS AES-128

#EXT-X-KEY:METHOD=AES-128
 

HLS AES-128는 콘텐츠를 암호화해서 전송하는 방식이다.

일반적으로 플레이어가 key를 가져와서 segment를 복호화할 수 있다.

즉 개념적으로 다음과 같다.

encrypted segment
       +
      key
       ↓
   decrypted media
 

DRM

반면:

SAMPLE-AES
Widevine
FairPlay
PlayReady
 

같은 DRM은 단순히:

"URL에서 key 하나 받아서 AES 복호화"

하는 구조와 다르다.

특히 브라우저에서는:

Encrypted Media Extensions
          ↓
      CDM
          ↓
    license server
          ↓
       decryption
 

같은 별도의 DRM 시스템이 들어간다.

따라서 HLS의 AES-128과 DRM을 같은 것으로 생각하면 안 된다.


5.6 HLS 예제

예를 들어 서버에 다음이 있다고 하자.

/master.m3u8
/1080p.m3u8
/720p.m3u8
/audio/ko.m3u8
/1080/init.mp4
/1080/001.m4s
/1080/002.m4s
...
 

Master:

#EXTM3U

#EXT-X-MEDIA:TYPE=AUDIO,
GROUP-ID="audio",
NAME="Korean",
DEFAULT=YES,
URI="audio/ko.m3u8"

#EXT-X-STREAM-INF:
BANDWIDTH=5000000,
RESOLUTION=1920x1080,
AUDIO="audio"

1080p.m3u8

#EXT-X-STREAM-INF:
BANDWIDTH=2500000,
RESOLUTION=1280x720,
AUDIO="audio"

720p.m3u8
 

플레이어는:

master.m3u8
     │
     ├───────────────┐
     ↓               ↓
1080p.m3u8       audio/ko.m3u8
     │               │
     ↓               ↓
init.mp4          audio init
     │               │
     ↓               ↓
001.m4s           audio001.m4s
002.m4s           audio002.m4s
003.m4s           audio003.m4s
 

를 받아서 재생한다.

ABR은 여기서 어떻게 작동하나?

처음에는 네트워크가 좋다고 판단해서:

1080p
 ↓
001.m4s
002.m4s
 

를 받고 있었는데 다운로드 속도가 떨어진다.

플레이어가:

현재 buffer = 4초
네트워크 = 불안정
 

이라고 판단하면:

1080p
 ↓
001
002
 

에서

720p
 ↓
003
004
 

로 바꿀 수 있다.

즉 variant를 바꾸는 것은 master playlist를 다시 받는 것이 아니라, 다른 variant의 media playlist/segment를 선택하는 것이다.

5.7 HLS 전체 구조를 하나의 그림으로 보면

                         HLS
                          │
                          ↓
                    master.m3u8
                          │
              #EXT-X-STREAM-INF
                          │
        ┌─────────────────┼─────────────────┐
        ↓                 ↓                 ↓
      1080p              720p              480p
        │                 │                 │
        ↓                 ↓                 ↓
   media.m3u8        media.m3u8        media.m3u8
        │
        ├── #EXT-X-MAP ──→ init.mp4
        │
        ├── #EXTINF ─────→ 001.m4s
        ├── #EXTINF ─────→ 002.m4s
        ├── #EXTINF ─────→ 003.m4s
        └── #EXT-X-ENDLIST
 

오디오가 별도라면:

                    master.m3u8
                         │
             ┌───────────┴───────────┐
             ↓                       ↓
          VIDEO                    AUDIO
             │                       │
       1080/720/480              Korean
             │                    English
             │                    Japanese
             ↓                       ↓
        video.m3u8              audio.m3u8
             │                       │
             ↓                       ↓
        video.m4s                audio.m4s
             │                       │
             └───────────┬───────────┘
                         ↓
                      Playback
 

결국 HLS의 핵심은 m3u8이 영상을 담고 있는 게 아니라, "어떤 미디어를 어떤 순서로 가져와야 하는지 알려주는 제어 파일"이라는 것이다.

MP4와 연결해 보면,

HLS = m3u8이라는 재생 지침 + (TS 또는 fMP4)라는 실제 미디어 segment들의 조합

특히 #EXT-X-STREAM-INF → variant 선택 → #EXT-X-MAP → moof/mdat → #EXTINF → segment → MSE/디코더까지 연결해서 보면, 실제 Chrome DevTools에서 HLS 하나를 잡았을 때 무슨 일이 일어나는지 거의 그대로 읽을 수 있다.

반응형