02 — HLS (HTTP Live Streaming)
이 문서가 답하는 질문: Apple의 표준은 어떻게 생겼고, 왜 m3u8 텍스트 두 단계로 나뉘어 있나? 표준 문서: RFC 8216 (HLS v7), Apple HLS Authoring Spec. 핵심 한 줄: HLS는 *텍스트 m3u8 두 단계 (Master → Media)*와 작은 segment 파일들만으로 비디오 스트리밍을 표현한다.
한 줄 답 (Pyramid Top)
HLS는 “playlist 안에 playlist” 구조다. 플레이어는 먼저 Master playlist에서 quality variant 목록을 읽고, 그중 하나의 Media playlist를 골라 segment를 시간순으로 받는다. 그게 전부다. 그 위에 자막·키 회전·바이트 범위·LL 같은 확장 태그가 얹혀 있을 뿐.
Why — 왜 HLS가 이 모양인가
HLS는 2009년 Apple이 iPhone 3GS의 비디오 재생을 만들면서 등장했다. 결정 기준 셋:
- HTTP 위에만 동작 — 기존 CDN/프록시/방화벽을 그대로 쓸 수 있어야 한다. RTSP/RTMP 같은 별도 프로토콜이면 모든 운영망에 구멍을 뚫어야 한다.
- 클라이언트가 다음 segment를 당겨 받는다 (pull) — 서버는 stateless. 한 사용자가 끊겨도 서버는 모른다 → 1억 명에게 같은 콘텐츠를 보낼 수 있다.
- 텍스트로 사람이 읽을 수 있어야 — 디버깅·운영·사람의 손으로 매니페스트 패치 가능.
이 셋의 합집합이 m3u8(텍스트 playlist) + segment(작은 파일들) 구조다. HTTP GET 한 번으로 끝나는 단순함이 표준 채택의 결정타였다.
How — Master와 Media의 두 단계
Master Playlist (variant 목록)
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac-128k",NAME="Korean",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="ko",CHANNELS="2",URI="audio/ko/audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac-128k",NAME="English",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="2",URI="audio/en/audio.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Korean",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="ko",URI="subs/ko.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="en",URI="subs/en.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=5300000,AVERAGE-BANDWIDTH=4800000,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=29.97,AUDIO="aac-128k",SUBTITLES="subs"
variant_1080p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,AVERAGE-BANDWIDTH=2500000,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=29.97,AUDIO="aac-128k",SUBTITLES="subs"
variant_720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,AVERAGE-BANDWIDTH=1200000,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=854x480,FRAME-RATE=29.97,AUDIO="aac-128k",SUBTITLES="subs"
variant_480p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=700000,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.97,AUDIO="aac-128k",SUBTITLES="subs"
variant_360p/index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=200000,CODECS="avc1.640028",RESOLUTION=1920x1080,URI="variant_1080p/iframe.m3u8"| 태그 | 의미 |
|---|---|
#EXTM3U | 모든 m3u8의 첫 줄 (마법 시그니처) |
#EXT-X-VERSION:7 | playlist 호환 버전 (LL은 9~) |
#EXT-X-INDEPENDENT-SEGMENTS | 모든 segment가 IDR로 시작 → 어디서든 시크 가능 |
#EXT-X-MEDIA | 부속 트랙 선언 (오디오·자막·비디오) |
#EXT-X-STREAM-INF | variant 선언 — 다음 줄의 URI가 Media playlist |
BANDWIDTH | peak bps (ABR 결정 핵심) |
AVERAGE-BANDWIDTH | 평균 bps (선택적) |
CODECS | RFC 6381 형식 (avc1.640028 = H.264 High@4.0) |
RESOLUTION | 가로×세로 |
FRAME-RATE | fps |
AUDIO, SUBTITLES | 위 #EXT-X-MEDIA GROUP-ID 참조 |
#EXT-X-I-FRAME-STREAM-INF | trick play용 I-frame only playlist |
→ Master는 카탈로그다. 어떤 segment도 직접 가리키지 않는다.
Media Playlist (실제 segment 목록)
VOD (완결된 영상)
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:6.006,
seg_00000.ts
#EXTINF:6.006,
seg_00001.ts
#EXTINF:6.006,
seg_00002.ts
...
#EXTINF:5.005,
seg_01234.ts
#EXT-X-ENDLIST| 태그 | 의미 |
|---|---|
#EXT-X-TARGETDURATION:6 | 가장 긴 segment의 상한 (정수 초). 플레이어의 폴링 주기 결정 |
#EXT-X-MEDIA-SEQUENCE:0 | 첫 segment의 시퀀스 번호 (라이브에서 sliding window 추적) |
#EXT-X-PLAYLIST-TYPE:VOD | 변경되지 않음 (라이브가 아님) |
#EXTINF:6.006 | 다음 segment의 실제 길이 (소수점 가능) |
#EXT-X-ENDLIST | 더 이상 추가 안 됨 (VOD 완결) |
29.97fps × 6초 = 179.82 frames가 정확히 안 떨어진다. 그래서 EXTINF가 정확히 6.0이 아니라 6.006 같은 값이 나온다. 이걸 정수로 반올림해 target만 쓰면 누적 오차가 생겨 A/V sync drift.
Live (계속 갱신)
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:1240
#EXT-X-DISCONTINUITY-SEQUENCE:2
#EXTINF:6.0,
seg_01240.ts
#EXTINF:6.0,
seg_01241.ts
#EXTINF:6.0,
seg_01242.ts라이브 m3u8은 움직이는 창이다. 플레이어는 매 target/2 초마다 이걸 다시 GET하고, 새로 추가된 segment를 받는다. MEDIA-SEQUENCE가 단조증가하므로 새 segment를 식별할 수 있다. ENDLIST가 없다는 게 라이브 신호.
What — 컨테이너: TS vs fMP4
HLS는 v3까지 MPEG-TS(.ts)만 지원했다. v4부터 fMP4(fragmented MP4, .m4s)를 지원한다.
| 축 | MPEG-TS (.ts) | fMP4 (.m4s) |
|---|---|---|
| 헤더 오버헤드 | 188 byte 패킷마다 4 byte → 약 2% | moof/mdat 박스 — 작음 |
| CMAF 호환 | ❌ | ✅ (DASH와 같은 컨테이너) |
| Subtitle 인-stream | TX3G/CEA-608/708 | WebVTT 별도 트랙 권장 |
| iOS 지원 | iOS 3.0+ (모두) | iOS 10+ |
| DRM 권장 | AES-128 (key 회전) | SAMPLE-AES, FairPlay/CENC/CBCS |
| H.264 기준 | ✅ | ✅ |
| HEVC | 가능하나 복잡 | ✅ 권장 |
| AV1 | 비표준 | ✅ |
→ 신규 시스템은 fMP4 HLS(=CMAF 호환)로 가는 것이 default 합의. TS는 legacy 호환과 AES-128 key 회전이 필요할 때만.
#EXT-X-MAP — fMP4의 init segment
fMP4는 init.mp4 (= ftyp + moov)를 먼저 받아야 디코더가 변환표를 만들 수 있다.
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-MAP:URI="init.mp4"
#EXTINF:6.0,
seg_00001.m4s
#EXTINF:6.0,
seg_00002.m4s#EXT-X-MAP이 init을 가리킨다. init은 한 번만 받고, 모든 segment(.m4s)가 그 위에 얹힌다.
What — 키 회전과 DRM
AES-128 (단순, TS 시대의 표준)
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-KEY:METHOD=AES-128,URI="https://license.example.com/key1",IV=0x9c7db8778570d05c3177c349fd9236aa
#EXTINF:10.0,
seg_00001.ts
#EXTINF:10.0,
seg_00002.ts
#EXT-X-KEY:METHOD=AES-128,URI="https://license.example.com/key2",IV=0xbf2acdb7df1198d3f7d7e1e7a8e1c6f3
#EXTINF:10.0,
seg_00003.ts
#EXTINF:10.0,
seg_00004.ts#EXT-X-KEY가 그 다음 segment부터의 복호화 키와 IV를 지정한다. 새 #EXT-X-KEY가 나오면 거기서부터 키가 바뀐다 → rolling key. 키 서버는 사용자 인증을 먼저 검사하고 키를 돌려준다.
SAMPLE-AES + FairPlay (Apple의 DRM)
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://license.example.com/fps?contentId=abc",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"skd://scheme — iOS의 FairPlay CDM이 알아본다.KEYFORMAT="com.apple.streamingkeydelivery"— FairPlay 식별.- 실제 키 교환은 EME
requestMediaKeySystemAccess('com.apple.fps.1_0')흐름.
자세한 라이선스 흐름은 07-drm.md에서 다룬다.
What — 자막 트랙 (subtitles)
# (Master에서)
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Korean",DEFAULT=YES,LANGUAGE="ko",URI="subs/ko.m3u8"자막의 m3u8 자체는 시간 축으로 분할된 .vtt 파일들:
# subs/ko.m3u8
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:60
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:60.0,
ko_00000.vtt
#EXTINF:60.0,
ko_00001.vtt
...
#EXT-X-ENDLISTVTT 자체는:
WEBVTT
X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000
00:00:01.500 --> 00:00:03.500
안녕하세요.
00:00:04.000 --> 00:00:06.000
스트리밍 챕터입니다.X-TIMESTAMP-MAP은 VTT 자체 시간과 *MPEG-TS PCR (90kHz clock)*을 매핑한다. fMP4에서는 보통 생략 가능.- 라이브에서는 자막도 같은 sliding window로 갱신된다.
Thumbnail track (썸네일 변형)
자막 트랙을 재활용해 썸네일 sprite를 운반할 수 있다 (HLS Spec의 Image Media Playlist 연관).
WEBVTT
00:00:00.000 --> 00:00:10.000
sprites_00.jpg#xywh=0,0,160,90
00:00:10.000 --> 00:00:20.000
sprites_00.jpg#xywh=160,0,160,90#xywh=x,y,w,h미디어 fragment로 sprite sheet의 한 컷을 가리킨다.- Shaka Player의
addThumbnailsTrackAPI가 이걸 직접 받는다.
What-if — 잘못 만들면 어떻게 깨지는가
| 증상 | 원인 | 처방 |
|---|---|---|
| iOS Safari가 “재생할 수 없음” | BANDWIDTH 누락된 variant, 또는 CODECS 잘못 | RFC 6381 codec string 정확히 |
| 라이브 시작 직후 “끊김” | MEDIA-SEQUENCE가 한 번 감소 | 재인코딩으로 인해 sequence 깨짐 — #EXT-X-DISCONTINUITY-SEQUENCE 증분 |
| 시크가 segment 경계로 튐 | segment가 GOP-unaligned 또는 INDEPENDENT-SEGMENTS 누락 | 인코딩 시 force_key_frames 정확히 |
| 자막이 비디오 시간보다 살짝 어긋남 | TS의 X-TIMESTAMP-MAP 부정확 | fMP4로 마이그레이션 또는 정확한 PCR 측정 |
| Android가 “재생 안 됨” | hls.js 미탑재 | 수동 plug-in 또는 Shaka로 전환 |
| key 서버 hit 폭주 | 키 회전 너무 잦음 | 키 회전 주기를 분 단위로 (10분 등) |
#EXTINF 정수 반올림 | A/V sync drift | 소수점 6자리까지 정확히 |
| TARGETDURATION 작은데 segment가 더 긺 | 플레이어가 매니페스트 폴링 missing | 모든 segment의 EXTINF ≤ TARGETDURATION 보장 |
Insight — 흥미로운 이야기
“왜 iOS Safari만 HLS를 native로 받는가”
Apple이 만들었으니까 — 그게 정답의 반이다. 정확히는 iOS는 MSE(Media Source Extensions)를 Safari에 노출하지 않는다. 즉 hls.js가 동작하려면 Browser가 MSE를 줘야 하는데 iOS Safari는 MSE를 (오랫동안) 안 준다 → 대신
<video src="master.m3u8">로 URL을 직접 받게 했다. iPad OS 13에서 MSE가 들어왔지만 iPhone은 여전히 기본은 HLS direct loading이다. 그래서 모든 OTT 서비스는 HLS를 반드시 만든다. DASH만 만들고 HLS를 빼면 iPhone 사용자 전체를 잃는다.
“fMP4 HLS의 출현 이유”
CMAF가 DASH와 HLS를 같은 segment로 운영하자는 합의를 했다. 그러면 인코딩과 저장 비용이 절반. 그런데 그러려면 HLS도 fMP4를 받아야 한다 → HLS v4(2016)에서 fMP4 추가. iOS 10 이상부터 지원. 즉 fMP4 HLS는 HLS의 진화가 아니라 DASH와의 화해다.
“HLS v9의 LL-HLS는 매니페스트 폴링을 부순다”
전통 HLS는 매니페스트를 target/2초마다 폴링한다 — 6초 segment면 3초마다. LL-HLS는 blocking GET과 delta update로 폴링을 거의 없애 버린다.
05-low-latency.md참고.
한 단락 요약 + Mermaid
HLS는 텍스트 m3u8 두 단계(Master/Media)와 작은 segment 파일들로 구성된 가장 단순한 스트리밍 포맷이다. Master는 카탈로그, Media는 실제 segment 시간표. 라이브는 sliding window로 같은 매니페스트를 다시 받는 식. TS → fMP4로 옮겨가며 DASH(CMAF)와 합쳐졌고, DRM은 AES-128/SAMPLE-AES/FairPlay 3단으로 깊어진다.
→ 다음 파일 03-mpeg-dash.md: 같은 일을 XML과 SegmentTemplate으로 푸는 또 다른 표준.