
HTTP Live Streaming. 애플이 2009년에 만든 스트리밍 프로토콜이다.
당시 웹에서 영상을 보려면 Adobe Flash 플러그인이 필요했다. Flash는 보안 구멍이 많았고, RTMP라는 전용 프로토콜에 전용 서버까지 있어야 했다. 무엇보다 아이폰에서 Flash가 돌아가지 않았다. 애플 입장에서 아이폰으로 영상을 보여줄 방법이 필요했고, 그 해답이 HLS였다.
핵심 아이디어는 영상을 짧은 조각으로 잘라서 HTTP로 전송하는 것이다. 클라이언트는 조각들을 순서대로 받아 이어 붙여 재생한다. HTTP 기반이므로 특수 서버가 필요 없고, 기존 웹 서버와 CDN 인프라를 그대로 쓸 수 있다. 방화벽도 통과하고, 캐싱도 된다. Flash가 완전히 퇴출된 이후 HLS는 사실상 업계 표준으로 자리잡았다.
HLS 스트리밍은 세 개의 컴포넌트로 나뉜다.
카메라 → [인코더] → [세그멘터] → [웹 서버 / CDN] → [플레이어]
인코더는 원본 영상을 H.264(또는 H.265)로 압축한다. MPEG-2 Transport Stream 포맷으로 감싸서 세그멘터에 넘긴다.
세그멘터가 핵심이다. Transport Stream을 받아 일정 길이의 파일 조각(세그먼트)으로 쪼개고, 그 목록을 담은 .m3u8 인덱스 파일을 만든다. 세그먼트 하나가 완성될 때마다 인덱스 파일도 갱신된다.
웹 서버 / CDN은 이 파일들을 HTTP로 배포한다. 클라이언트는 먼저 인덱스 파일을 받고, 거기에 적힌 세그먼트 URL들을 순서대로 요청한다.

#EXT-X-ENDLIST 태그가 있으면 — 완성된 VOD다. 플레이어는 플레이리스트를 한 번만 다운로드하고 이후 재다운로드하지 않는다.
#EXT-X-ENDLIST 태그가 없으면 — 실시간 라이브다. 플레이어는 매 세그먼트 길이마다 플레이리스트를 다시 받으면서 새 세그먼트와 암호화 키를 재생 큐에 추가한다. 이 루프는 #EXT-X-ENDLIST가 나타날 때까지 계속된다.
m3u8은 UTF-8로 인코딩된 텍스트 파일이다. 두 종류가 있다.
"어떤 품질의 스트림이 있는지" 알려주는 파일이다. 실제로 열어보면 이렇게 생겼다.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=7532800,AVERAGE-BANDWIDTH=3440800,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1080x1920,FRAME-RATE=30.000
f20f6577e6d94a69ae0c954ed84db393_3.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4540800,AVERAGE-BANDWIDTH=2085600,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=720x1280,FRAME-RATE=30.000
f20f6577e6d94a69ae0c954ed84db393_2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2041600,AVERAGE-BANDWIDTH=950400,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=360x640,FRAME-RATE=30.000
f20f6577e6d94a69ae0c954ed84db393_1.m3u8
하나씩 보면:
#EXTM3U — 이 파일이 M3U8 포맷임을 선언하는 첫 줄#EXT-X-VERSION:3 — HLS 프로토콜 버전. 버전마다 쓸 수 있는 기능이 다르다#EXT-X-INDEPENDENT-SEGMENTS — 각 세그먼트가 이전 세그먼트 없이도 독립적으로 디코딩 가능하다는 선언. 화질 전환할 때 이전 세그먼트의 상태를 들고 올 필요가 없어서 ABR 전환이 깔끔해진다#EXT-X-STREAM-INF — 각 스트림의 상세 정보HLS 버전 이야기
#EXT-X-VERSION:3이 그냥 숫자가 아니다. 버전마다 쓸 수 있는 기능이 달라진다.
| 버전 | iOS | 추가된 주요 기능 |
|---|---|---|
| 1 | 3.0 | HLS 최초 버전 |
| 2 | 3.1 | EXT-X-DISCONTINUITY, 중복 스트림, IV 속성 |
| 3 | 4.2 | EXTINF 소수점 지원, CEA-608 자막 |
| 4 | 5.0 | I-Frame 플레이리스트, EXT-X-MEDIA, 바이트 레인지 |
| 5 | 6.0 | EXT-X-MAP, 자막 지원 강화 |
| 7+ | — | fMP4 세그먼트, EXT-X-ALLOW-CACHE deprecated |
실서비스 파일이 버전 3을 쓰는 이유는 EXTINF 값에 소수점이 필요하기 때문이다. #EXTINF:1.00000 같은 표현이 버전 3부터 유효하다. 버전이 낮으면 EXTINF 값이 정수여야 하고, 그러면 seek 정밀도가 떨어진다.
마스터 플레이리스트와 미디어 플레이리스트의 관계는 아래와 같다.

마스터 플레이리스트가 가리키는 실제 영상 조각 목록이다.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:1.00000,
f20f6577e6d94a69ae0c954ed84db393_2_00001.ts
#EXTINF:1.00000,
f20f6577e6d94a69ae0c954ed84db393_2_00002.ts
...
#EXTINF:0.03333,
f20f6577e6d94a69ae0c954ed84db393_2_04282.ts
#EXT-X-ENDLIST
#EXT-X-TARGETDURATION:1 — 세그먼트 최대 길이. Apple 공식 권장치는 5~10초다#EXT-X-MEDIA-SEQUENCE:1 — 첫 번째 세그먼트의 시퀀스 번호. 라이브 스트리밍에서 플레이어가 어느 시점부터 받아야 하는지 추적하는 용도#EXTINF:1.00000 — 다음에 오는 세그먼트 파일의 재생 길이(초)#EXT-X-ENDLIST — 더 이상 세그먼트가 없다는 선언세그먼트 포맷 — .ts와 fMP4
위 파일에서 세그먼트가 .ts 확장자를 쓰고 있다. .ts는 MPEG-2 Transport Stream의 약자로, HLS 초창기부터 써온 포맷이다. 2016년 WWDC에서 fMP4(fragmented MP4) 지원이 추가됐다. fMP4는 CMAF(Common Media Application Format)을 지원하는데, 같은 세그먼트로 HLS와 DASH를 동시에 서비스할 수 있어서 인코딩을 한 번만 해도 된다.
미디어 플레이리스트는 세 가지 타입이 있다. 핵심은 #EXT-X-ENDLIST 태그 하나다.
이 태그가 있으면 플레이어는 플레이리스트를 한 번만 다운로드하고 이후 재다운로드를 멈춘다. 완성된 VOD이기 때문이다.
이 태그가 없으면 플레이어는 "아직 진행 중인 라이브"라고 판단한다. 매 세그먼트 길이마다 플레이리스트를 다시 받으면서 새 세그먼트를 재생 큐에 추가한다. 이 루프는 #EXT-X-ENDLIST가 나타날 때까지 계속된다.
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:9.9,
segment0.ts
...
#EXT-X-ENDLIST
사실상 정적 파일.
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:26
#EXTINF:9.9,
segment26.ts
#EXTINF:9.9,
segment27.ts
← #EXT-X-ENDLIST 없음
새 세그먼트가 추가될 때마다 오래된 세그먼트는 앞에서 제거된다.
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-TARGETDURATION:10
#EXTINF:9.9,
segment0.ts
...
← 방송 중에는 #EXT-X-ENDLIST 없음
EVENT를 선언하면 세그먼트를 앞에서 지울 수 없다. 시청자가 방송을 처음부터 볼 수 있다. 방송이 끝나면 #EXT-X-ENDLIST를 추가하는 것만으로 VOD로 전환된다.
HLS의 핵심 기능으로, 네트워크에 따라 화질이 자동으로 바뀌는 원리를 의미한다.
마스터 플레이리스트는 여러 개의 미디어 플레이리스트를 가리킨다. 플레이어는 이 중 하나를 골라 재생하다가 네트워크 상황에 따라 다른 플레이리스트로 전환한다.
| Variant | 해상도 | 최대 대역폭 | 평균 대역폭 |
|---|---|---|---|
_3.m3u8 | 1080×1920 | 7.5 Mbps | 3.4 Mbps |
_2.m3u8 | 720×1280 | 4.5 Mbps | 2.1 Mbps |
_1.m3u8 | 360×640 | 2.0 Mbps | 0.95 Mbps |
동작 방식:
전환이 세그먼트 단위로 일어나기 때문에, 사용자는 영상이 잠깐 뭉개지는 정도만 느끼고 끊기지는 않는다.
BANDWIDTH vs AVERAGE-BANDWIDTH
플레이어는 Variant 전환 시점을 결정할 때 BANDWIDTH를 기준으로 쓴다. AVERAGE-BANDWIDTH는 실제 소요 대역폭 예측에 쓴다. BANDWIDTH를 실제보다 너무 높게 설정하면 플레이어가 필요 이상으로 낮은 화질을 고집한다. 반대로 너무 낮게 설정하면 네트워크가 부족한 상황에서도 높은 화질을 시도해 버퍼링이 생긴다.
마스터 플레이리스트에는 같은 대역폭의 Variant를 여러 개 넣을 수 있다. 이는 단순한 중복이 아니라 장애 폴백 용도다.

실서비스 파일 기준으로 오리진 서버가 두 개라면 이렇게 구성할 수 있다.
#EXT-X-STREAM-INF:BANDWIDTH=4540800,AVERAGE-BANDWIDTH=2085600,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=720x1280,FRAME-RATE=30.000
https://origin-a.example.com/streams/f20f6577_2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4540800,AVERAGE-BANDWIDTH=2085600,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=720x1280,FRAME-RATE=30.000
https://origin-b.example.com/streams/f20f6577_2.m3u8
origin-a에서 404 에러가 나거나 서버가 다운되면, 플레이어는 같은 BANDWIDTH의 다음 항목인 origin-b로 자동 전환한다. 사용자는 끊김 없이 재생을 이어간다.

HLS 스펙에 따르면 플레이어는 안정적인 재생을 위해 TARGETDURATION의 3배만큼 버퍼를 유지해야 한다. Apple 권장 세그먼트 길이가 6초이니, 이것만으로도 최소 18초의 지연이 기본값으로 깔린다.
현실에서는 더 심하다. 라이브의 경우 인코더가 세그먼트를 만드는 시간, 오리진 서버에 업로드하는 시간, CDN 엣지 서버로 전파되는 시간, 플레이어가 다운로드하고 디코딩하는 시간이 전부 쌓인다. 실제 유리-화면 지연(Glass-to-Glass Delay)은 20~30초까지 벌어진다.
라이브커머스에서 호스트가 "지금 구매 버튼 누르세요"라고 했는데, 시청자가 그 말을 20초 뒤에 듣는다면 의미가 없다.
Apple이 2019년 WWDC에서 발표했다. 기존 HLS와 완전히 하위 호환되는 방식으로, 지연을 2~5초 수준까지 줄인다. LL-HLS를 모르는 구형 플레이어는 새 태그들을 무시하고 일반 HLS로 폴백한다.
핵심 개념은 세 가지다.
1. Partial Segments
기존 6초짜리 세그먼트를 더 작은 파트(Part)로 쪼개서 배포한다. 세그먼트가 완성되기 전에 파트 단위로 먼저 내보낸다. 플레이어는 전체 세그먼트를 기다리지 않고 파트가 나오는 즉시 받아 재생한다.
fileSequence272.mp4
#EXT-X-PART:DURATION=0.5,URI="filePart273.0.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.5,URI="filePart273.1.mp4"
#EXT-X-PART:DURATION=0.5,URI="filePart273.2.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4"
#EXT-X-PRELOAD-HINT는 아직 존재하지 않는 다음 파트의 URI를 미리 알려준다. 플레이어는 이 힌트를 보고 미리 요청을 걸어두고, 서버는 준비되는 순간 응답한다.
2. Blocking Playlist Reload
플레이어가 "어떤 버전의 플레이리스트가 필요한지"를 쿼리 파라미터로 명시하고, 서버는 그 버전이 준비될 때까지 응답을 붙잡고 있다가 준비되면 바로 내려준다.
GET /playlist.m3u8?_HLS_msn=1803&_HLS_part=2
서버가 LL-HLS를 지원한다는 사실은 #EXT-X-SERVER-CONTROL 태그로 선언한다.
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.5
3. Playlist Delta Updates
파트가 0.5초 단위로 나오면, 플레이리스트도 그만큼 자주 갱신된다. ?_HLS_skip=YES 파라미터를 붙이면 서버는 이전 버전과의 차이만 내려준다.
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
4. Rendition Reports
LL-HLS 환경에서 ABR 전환 비용을 줄이는 기법이다.
일반 HLS에서 화질을 전환하려면 새 Variant의 플레이리스트를 처음부터 받아야 한다. 그 플레이리스트가 얼마나 최신인지 모르기 때문이다. 재생 중인 1Mbps 플레이리스트가 시퀀스 1803까지 왔다면, 2Mbps 플레이리스트도 지금쯤 어디까지 왔는지 알아야 바로 그 시점으로 접근할 수 있다.
#EXT-X-RENDITION-REPORT 태그가 이 정보를 플레이리스트에 담아준다. 현재 받는 플레이리스트 업데이트에 다른 Variant의 최신 시퀀스 번호와 파트 번호가 포함된다.
#EXT-X-RENDITION-REPORT:URI="../2m/playlist.m3u8",LAST-MSN=1803,LAST-PART=2
클라이언트는 이 정보를 보고 2Mbps 플레이리스트의 최신 위치를 바로 알 수 있다. 처음부터 받을 필요 없이 ?_HLS_msn=1803&_HLS_part=2로 바로 그 시점을 요청하면 된다. LL-HLS에서 ABR 전환이 끊기지 않고 빠르게 이루어지는 이유가 이것이다.
매일같이 플레이어를 다루면서도 .m3u8 파일을 열어볼 때마다 낯설어하는 게 싫어서 정리해봤습니다.
참고