HLS(HTTP Live Streaming) 레퍼런스 — M3U8·ABR·암호화·LL-HLS
이 글의 핵심
HLS는 HTTP 기반 적응형 스트리밍 사실상의 표준입니다. 이 글은 RFC 8216·확장 태그, TS와 fMP4(CMAF) 세그먼트, FFmpeg로의 인코딩·암호화·ABR, FairPlay, CDN, LL-HLS(저지연), 그리고 hls.js·Video.js·Safari에서의 구현·오류 처리까지 한 권짜리 참조로 정리합니다.
HLS(HTTP Live Streaming)는 미디어 세그먼트와 재생 목록(m3u8)을 일반 HTTP로 전달하는 적응형 스트리밍 형식입니다. IETF RFC 8216으로 문서화되어 있으며, 마스터 플레이리스트가 여러 variant 스트림을 가리키고, 각 미디어 플레이리스트가 실제 TS 또는 fMP4 조각의 URL과 메타데이터를 나열하는 구조로 이해하면 됩니다. 이 글은 스펙 해석, FFmpeg 인코딩, 암호화와 DRM, 저지연(LL-HLS), 배포, 브라우저 구현을 참조서 형태로 묶습니다.
HLS는 시간이 지나면서 확장 태그와 Apple·방송 업계의 구현이 계속 추가되어 왔습니다. 운영할 때는 스펙 문서보다 대상 클라이언트가 실제로 해석하는 태그 집합을 기준으로 검증하십시오.
프로토콜 스펙 (RFC 8216)
M3U8 포맷
HLS 플레이리스트는 텍스트 파일이며 확장자는 관례적으로 m3u8입니다. RFC 8216 §4.1에 따르면 플레이리스트는 반드시 BOM 없는 UTF-8이어야 하고, 첫 줄은 #EXTM3U여야 합니다. #EXT-X-VERSION은 버전 1을 넘는 기능을 쓸 때 반드시 넣어야 하며, 생략하면 버전 1 규칙을 따르는 것으로 간주됩니다.
- 주석과 태그:
#으로 시작하는 줄 중#EXT로 시작하는 것이 태그이고, 나머지는 주석입니다. - URI 줄:
#으로 시작하지 않는 줄은 세그먼트나 다른 플레이리스트의 URI(상대 또는 절대)입니다. - 줄 끝: LF 또는 CRLF를 쓸 수 있습니다.
구문을 해석할 때는 같은 태그가 여러 번 나올 때의 규칙(예: 키 회전 시 EXT-X-KEY는 이후 세그먼트 전체에 적용)과, 버전 3부터 EXTINF 길이를 소수로 쓸 수 있다는 점을 함께 봐야 합니다.
Master Playlist vs Media Playlist
| 구분 | Master Playlist | Media Playlist |
|---|---|---|
| 역할 | variant 스트림 목록(해상도·비트레이트 등) | 미디어 세그먼트 나열과 메타데이터 |
| 흔한 태그 | EXT-X-STREAM-INF, EXT-X-MEDIA, (선택) EXT-X-I-FRAME-STREAM-INF | EXT-X-TARGETDURATION, EXT-X-MEDIA-SEQUENCE, EXTINF, EXT-X-KEY, EXT-X-MAP |
| URI | EXT-X-STREAM-INF 다음 줄에 variant URL | 각 세그먼트(또는 byte range) URI |
- 클라이언트(ABR): 마스터를 받아 대역폭과 코덱 지원 여부에 따라 variant를 고른 뒤, 그 variant의 미디어 플레이리스트를 받아 세그먼트를 다운로드합니다. 전환하면 다른 미디어 플레이리스트로 옮겨 갑니다.
- VOD vs 라이브:
EXT-X-ENDLIST가 있으면 더 이상 세그먼트가 추가되지 않는 VOD이고, 없으면 클라이언트가 플레이리스트를 주기적으로 다시 받는 라이브(슬라이딩 윈도우)입니다.EXT-X-PLAYLIST-TYPE으로VOD나EVENT를 명시할 수도 있습니다.
EXT-X 태그 (핵심·참조)
아래는 RFC 8216에 정의된 주요 태그입니다. 모든 서비스와 플레이어가 똑같이 지원하지는 않으므로, 지원 여부는 대상 플레이어별로 확인해야 합니다.
| 태그 | 용도 |
|---|---|
EXTM3U | 확장 M3U 형식임을 표시(첫 줄) |
EXT-X-VERSION | 플레이리스트가 요구하는 호환 버전 |
EXT-X-INDEPENDENT-SEGMENTS | 모든 세그먼트가 앞 세그먼트 없이 독립적으로 디코딩 가능함을 선언(마스터·미디어 모두 가능) |
EXT-X-TARGETDURATION | 세그먼트 최대 길이(초, 반올림한 정수) |
EXT-X-MEDIA-SEQUENCE | 플레이리스트 첫 세그먼트의 시퀀스 번호 |
EXT-X-DISCONTINUITY-SEQUENCE | 슬라이딩 윈도우에서 사라진 불연속 개수(렌디션 간 동기용) |
EXT-X-PLAYLIST-TYPE | VOD 또는 EVENT(추가만 되고 삭제는 없는 라이브) |
EXT-X-I-FRAMES-ONLY | I-frame만 담은 플레이리스트임을 표시(트릭 플레이) |
EXT-X-START | 재생 시작 위치 지정 |
EXT-X-ENDLIST | 더 이상 세그먼트가 추가되지 않음(VOD 끝) |
EXT-X-KEY | 이후 세그먼트의 암호화 방식과 키 URI |
EXT-X-MAP | fMP4 등의 초기화 세그먼트(init) |
EXT-X-PROGRAM-DATE-TIME | 세그먼트 첫 샘플의 절대 시각(동기·광고 등) |
EXT-X-DATERANGE | 시간 구간에 붙는 메타데이터(광고 큐, 이벤트 등) |
EXTINF | 다음 URI 세그먼트의 길이(초) |
EXT-X-BYTERANGE | 다음 URI의 일부 바이트 범위만 세그먼트로 사용 |
EXT-X-DISCONTINUITY | 인코딩 설정이나 타임스탬프가 끊기는 지점 표시 |
EXT-X-MEDIA (마스터) | 대체 오디오·자막 렌디션 그룹 |
EXT-X-STREAM-INF (마스터) | variant 속성, 다음 줄이 variant URI |
EXT-X-I-FRAME-STREAM-INF (마스터) | I-frame 전용 플레이리스트(빨리 감기·썸네일 탐색용) |
EXT-X-SESSION-KEY (마스터) | 미디어 플레이리스트를 받기 전에 키를 미리 로드할 수 있도록 알려 줌 |
EXT-X-SESSION-DATA (마스터) | 세션 단위 임의 메타데이터(DATA-ID, VALUE 또는 URI) |
RFC 8216 §4에 문법이, §8에 플레이리스트 예가 있습니다. EXT-X-STREAM-INF의 BANDWIDTH, RESOLUTION, CODECS는 ABR 선택과 호환성 판단에 직접 쓰입니다.
Transport Stream(TS) vs fMP4(CMAF) 세그먼트
- MPEG-2 TS(
.ts): 전통적인 HLS 세그먼트입니다. 각 세그먼트에 PAT/PMT가 들어 있어 따로 떼어 디코딩하기 쉽고, 방송·레거시 인코더가 많이 씁니다. - fMP4(Fragmented MP4) / CMAF: 초기화 세그먼트(
EXT-X-MAP)와 미디어 세그먼트가 짝을 이룹니다(RFC 8216 §3.3). 같은 CMAF 세그먼트를 DASH와 HLS가 함께 쓸 수 있다는 점이 가장 큰 장점이고, HEVC 같은 코덱은 Apple이 fMP4로만 지원하는 경우가 있습니다.
선택 기준은 호환성입니다. CDN, 클라이언트, 인코더가 모두 fMP4를 지원하면 CMAF/fMP4로 가는 편이 중복 패키징과 저장을 줄이는 데 유리하고, 오래된 셋톱박스나 툴체인을 지원해야 하면 TS가 여전히 흔합니다.
바이트 레인지(Byte Range)
EXT-X-BYTERANGE는 다음 URI 리소스의 일부 바이트 범위만 세그먼트로 쓴다는 뜻입니다(RFC 8216 §4.3.2.2). 형식은 길이[@오프셋]이고, 오프셋을 생략하면 같은 URI의 직전 범위 바로 다음부터 이어집니다. 세그먼트 파일 수천 개 대신 큰 파일 하나에 여러 세그먼트를 담을 수 있지만, 오리진과 CDN이 Range 요청과 그 캐싱을 제대로 지원해야 합니다. 이 태그를 쓰려면 버전 4 이상이어야 합니다.
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.0,
#EXT-X-BYTERANGE:821000@0
archive.ts
#EXTINF:6.0,
#EXT-X-BYTERANGE:790400
archive.ts
#EXT-X-ENDLIST
두 번째 세그먼트는 오프셋이 없으므로 첫 세그먼트가 끝난 821000번째 바이트부터 790400바이트를 읽습니다. 각 세그먼트에는 여전히 EXTINF가 필요합니다.
실전 구현
FFmpeg로 HLS 생성 (옵션 정리)
FFmpeg는 -f hls 먹서로 HLS를 출력합니다. 주요 -hls_* 옵션을 용도별로 묶으면 다음과 같습니다. 버전에 따라 옵션이 다를 수 있으니 ffmpeg -h muxer=hls로 확인하십시오.
| 옵션 | 설명 |
|---|---|
-hls_time | 목표 세그먼트 길이(초). 키프레임에서만 자르므로 실제 길이는 달라질 수 있음 |
-hls_list_size | 플레이리스트에 유지할 세그먼트 개수. 0이면 전부 유지(VOD), 양수면 라이브 슬라이딩 윈도우 |
-hls_delete_threshold / -hls_flags delete_segments | 윈도우에서 빠진 오래된 세그먼트 파일 삭제(디스크 관리) |
-hls_base_url | 세그먼트 URL 앞에 붙는 베이스 |
-hls_segment_filename | 세그먼트 파일명 패턴 |
-hls_segment_type | mpegts 또는 fmp4 |
-hls_fmp4_init_filename | fMP4 init 파일명 |
-hls_key_info_file | AES-128 암호화용 키 정보 파일 |
-hls_flags | independent_segments, program_date_time, append_list, temp_file, round_durations 등 플래그 조합 |
-hls_start_number_source | 시작 시퀀스 번호 정책 |
-master_pl_name / -hls_playlist_type / -streaming | 마스터 플레이리스트 생성, event/vod 타입 지정 |
-force_key_frames (비디오) / -g (GOP) | 키프레임 강제 — 세그먼트 길이와 직결 |
FFmpeg HLS muxer 문서에 나오는 추가 옵션 중 자주 쓰는 것은 다음과 같습니다.
| 옵션 | 설명 |
|---|---|
-start_number / -hls_start_number_source | MEDIA-SEQUENCE와 파일명 번호(generic / epoch / epoch_us / datetime) |
-hls_allow_cache | EXT-X-ALLOW-CACHE 태그 출력(버전 7에서 제거된 레거시 태그) |
-hls_delete_threshold | delete_segments와 함께, 윈도우 밖 세그먼트를 디스크에 몇 개 더 남겨 둘지 |
-hls_segment_options | 내부 MPEG-TS 먹서에 넘기는 key=value (콜론 이스케이프 주의) |
-hls_enc / -hls_enc_key / -hls_enc_key_url / -hls_enc_iv | 키 정보 파일 없이 AES 옵션을 직접 줄 때 |
-strftime / -strftime_mkdir | 세그먼트 파일명에 날짜·시간 치환, 하위 폴더 생성 |
-master_pl_name / -master_pl_publish_rate | 마스터 m3u8 이름과 갱신 주기(다중 variant와 함께) |
-method / -http_user_agent | HLS 출력을 HTTP PUT 등으로 원격 오리진에 업로드할 때 |
전역 -output_ts_offset | 출력 타임스탬프 오프셋. program_date_time과 함께 절대 시각을 맞출 때 쓰기도 함 |
ABR(동일 소스, 두 품질 + 마스터) — FFmpeg 문서의 var_stream_map 예 패턴:
ffmpeg -i input.ts -b:v:0 1000k -b:v:1 256k -b:a:0 64k -b:a:1 32k \
-map 0:v -map 0:a -map 0:v -map 0:a -f hls \
-var_stream_map "v:0,a:0 v:1,a:1" \
-hls_time 4 -hls_list_size 6 -master_pl_name master.m3u8 \
-hls_segment_filename 'seg_%v_%03d.ts' stream_%v.m3u8
해상도까지 나누려면 filter_complex로 [v0][v1]처럼 스케일된 스트림을 만든 뒤 각각 -map하는 식으로 확장합니다. GOP와 force_key_frames가 hls_time과 맞는지는 항상 검증하십시오. 키프레임 간격이 세그먼트 길이의 약수가 아니면 세그먼트 길이가 들쭉날쭉해지고 EXT-X-TARGETDURATION이 커집니다.
기본 fMP4 + 독립 세그먼트 예시:
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -b:a 128k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-f hls -hls_time 6 -hls_list_size 0 \
-hls_segment_type fmp4 -hls_flags independent_segments \
-hls_fmp4_init_filename init.mp4 \
-var_stream_map "v:0,a:0" \
-master_pl_name master.m3u8 out_%v/stream.m3u8
출력 경로의 %v는 -var_stream_map의 variant 번호로 치환되므로, variant가 하나여도 맵을 지정해 두는 것이 안전합니다. 여러 variant(ABR)는 위의 -var_stream_map 패턴으로 한 번에 마스터까지 만들거나, 비트레이트별로 따로 인코딩한 뒤 마스터를 스크립트로 합칩니다.
적응형 비트레이트(ABR)
마스터에 여러 EXT-X-STREAM-INF가 있고, 클라이언트는 측정한 대역폭, 버퍼 상태, 디코더 능력을 보고 variant를 바꿉니다. RFC 8216 §4.3.4.2의 BANDWIDTH(최대치), AVERAGE-BANDWIDTH, CODECS, RESOLUTION 등이 선택에 쓰입니다.
운영하면서 확인할 점은 다음과 같습니다.
- 비트레이트 래더의 모든 variant가 같은 GOP·키프레임 위치를 쓰도록 맞춰야 전환 시점의 품질 저하가 줄어듭니다.
- 오디오가
EXT-X-MEDIATYPE=AUDIO로 분리된 구성인지, 비디오 세그먼트에 함께 먹싱된 구성인지에 따라 플레이어 설정이 달라집니다. - 선언한
BANDWIDTH가 실제 세그먼트 비트레이트와 크게 다르면 ABR이 대역을 과소·과대 추정합니다.BANDWIDTH는 세그먼트 비트레이트의 최대치여야 하므로, 인코딩 후 실제 세그먼트 크기로 검산하십시오.
마스터 m3u8(개념):
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.4d001e,mp4a.40.2"
360p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.2"
720p/playlist.m3u8
AES-128 암호화
RFC 8216 §4.3.2.4의 EXT-X-KEY에 METHOD=AES-128을 지정하면 세그먼트 전체를 AES-128-CBC로 암호화하고, 키를 받을 URL을 URI로 알려 줍니다. 키와 세그먼트는 HTTPS로 내려 전송 구간을 보호하는 것이 일반적입니다.
#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key?kid=1",IV=0x...
- 키 회전: 세그먼트 구간마다 새
EXT-X-KEY를 넣으면 키를 교체할 수 있습니다. 플레이어가 태그가 바뀔 때마다 새 키를 받아야 합니다. - IV:
IV를 생략하면 세그먼트의 미디어 시퀀스 번호가 IV로 쓰입니다. - FFmpeg:
-hls_key_info_file로 키 URI, 로컬 키 파일 경로, (선택) IV를 담은 파일을 지정합니다.
한계: 키 URL에 접근할 수 있는 사람은 누구나 키를 받아 세그먼트를 복호화할 수 있습니다. 키 URL에 인증을 걸어도 정상 사용자가 키를 추출해 공유하는 것까지는 막지 못합니다. 샘플 단위로 암호화하는 SAMPLE-AES도 이 점은 같습니다. 라이선스 계약상 복제 방지가 필요하면 DRM을 검토하십시오.
DRM(예: Apple FairPlay Streaming)
FairPlay Streaming(FPS)은 HLS에 붙는 Apple의 DRM입니다. 패키징 도구가 콘텐츠를 SAMPLE-AES(fMP4는 cbcs)로 암호화하고 EXT-X-KEY에 KEYFORMAT="com.apple.streamingkeydelivery"를 넣습니다. 재생 시 클라이언트는 SPC(Server Playback Context)를 만들어 키 서버의 KSM(Key Security Module)에 보내고, CKC(Content Key Context)를 받아 복호화합니다. Apple에서 FPS 자격 증명을 발급받아야 하므로 FFmpeg만으로 완결되는 경우는 드뭅니다. 실무 흐름은 대략 다음과 같습니다.
- 지원 도구로 HLS(보통 fMP4)와 DRM 메타데이터를 함께 패키징합니다.
- KSM·라이선스 서버가 클라이언트 정책에 맞게 키를 내려 줍니다.
- 플레이어는 AVPlayer(네이티브 앱)나 Safari의 EME(
com.apple.fps)를 씁니다.
OTT에서 FairPlay는 HLS 태그 몇 줄의 문제가 아니라 키 서버, 인증, 라이선스 정책을 포함한 서버 구성 문제입니다. 사용하는 패키저·DRM 벤더(AWS Elemental, Wowza 등)의 가이드와 Apple FPS 문서를 함께 봐야 합니다.
CDN 배포 전략
- 캐시 TTL:
m3u8은 짧은 TTL을 주거나 쉽게 무효화할 수 있어야 합니다. 라이브 플레이리스트가 오래 캐시되면 시청자가 과거 시점에 멈춥니다. 세그먼트(.ts/.m4s)는 내용이 바뀌지 않으므로 긴Cache-Control: max-age와immutable이 잘 맞습니다. - CORS / HTTPS: hls.js처럼
fetch/XHR로 매니페스트·세그먼트·키를 받는 웹 플레이어에서는 CORS가 필수입니다.Access-Control-Allow-Origin과 자격 증명 정책을 m3u8, 세그먼트, 키 모두에 맞추십시오. - 쿼리스트링·서명 URL:
?token=방식이면 CDN이 쿼리를 캐시 키에 포함하는지 확인해야 합니다. 포함하면 사용자마다 캐시 객체가 따로 생겨 히트율이 무너지고, 제외하면 토큰 검증을 엣지에서 따로 해야 합니다. - 라이브 오리진 보호: 라이브에서는 플레이리스트 요청이 몇 초마다 모든 시청자에게서 오므로, 오리진 앞에 쉴드 캐시를 두고 필요하면 멀티 CDN을 검토합니다.
HTTP 캐시와 Range 동작은 RFC 9110 (HTTP Semantics)과 캐시 RFC(RFC 9111)를 함께 참고하십시오.
고급 주제: LL-HLS 및 저지연
저지연 HLS(LL-HLS)는 RFC 8216 본문에는 없고, Apple이 발표한 뒤 RFC 8216의 개정 초안(draft-pantos-hls-rfc8216bis)에 편입된 확장입니다. 태그와 동작은 초안 버전에 따라 조금씩 달라졌으므로, 사용하는 패키저와 플레이어가 어느 버전을 따르는지 대조해야 합니다. 아래는 개념 정리입니다.
Partial Segments(파트)
세그먼트가 완성될 때까지 기다리지 않고, 그보다 작은 파트(예: 200ms~1초) 단위로 플레이리스트에 공개해 인코더에서 플레이어까지의 경로를 짧게 만듭니다. 세그먼트 자체는 여전히 수 초 길이로 유지되므로, 파트는 라이브 엣지 근처에서만 쓰이고 오래된 구간은 일반 세그먼트로 재생됩니다.
HTTP/2 Server Push
초기 LL-HLS 제안은 HTTP/2 Server Push로 다음 파트를 플레이리스트 응답과 함께 밀어 주는 방식을 요구했습니다. 그러나 Apple은 2020년 개정에서 이 요구를 없애고 프리로드 힌트로 대체했고, Chrome도 2022년(Chrome 106)에 HTTP/2 Server Push 지원을 기본 비활성화했습니다. 실서비스에서는 Push를 가정하지 말고 프리로드 힌트, 캐시, 오리진 배치를 우선하십시오.
Preload Hints
EXT-X-PRELOAD-HINT는 아직 완성되지 않은 다음 파트의 URI를 미리 알려 줍니다. 클라이언트가 이 URI를 바로 요청하면 서버는 파트가 만들어지는 대로 응답을 흘려보내므로, 플레이리스트를 다시 받고 요청하는 왕복 한 번이 줄어듭니다. TYPE(PART/MAP)과 URI 규칙은 패키저와 hls.js 버전이 서로 맞아야 합니다.
Blocking Playlist Reload
서버가 EXT-X-SERVER-CONTROL에 CAN-BLOCK-RELOAD=YES를 선언하면, 클라이언트는 _HLS_msn(미디어 시퀀스 번호)과 _HLS_part 쿼리 파라미터로 “이 세그먼트/파트가 포함된 플레이리스트를 달라”고 요청할 수 있습니다. 서버는 해당 파트가 생길 때까지 응답을 붙잡아 두었다가 돌려줍니다. 짧은 주기로 폴링하며 변화 없는 플레이리스트를 계속 받는 낭비가 사라지고, 새 파트가 생기는 즉시 알 수 있습니다. 오리진은 대기 중인 연결 수와 타임아웃을 함께 관리해야 하며, CDN이 이런 쿼리 파라미터 요청을 올바르게 캐싱·병합하는지도 확인해야 합니다.
LL-HLS 관련 확장 태그(참조)
아래는 LL-HLS 사양과 패키저가 널리 쓰는 태그·속성입니다. 버전에 따라 hls.js, Safari, tvOS의 지원이 다르며, RFC 8216 본문에는 없으므로 최신 8216bis 초안과 Apple HLS Authoring Specification으로 교차 확인하십시오.
| 태그·속성 | 용도(요지) |
|---|---|
EXT-X-SERVER-CONTROL | CAN-BLOCK-RELOAD, CAN-SKIP-UNTIL, HOLD-BACK, PART-HOLD-BACK 등 서버 기능 선언 |
EXT-X-PART-INF | PART-TARGET(파트 목표 길이) 선언 |
EXT-X-PART | 파트 하나의 길이·URI·독립성(INDEPENDENT) |
EXT-X-PRELOAD-HINT | 다음에 요청할 리소스 힌트 |
EXT-X-RENDITION-REPORT | 다른 렌디션의 최신 미디어 시퀀스·파트 번호. ABR 전환 시 바로 올바른 위치를 요청하게 해 줌 |
EXT-X-SKIP | 델타 플레이리스트에서 이미 받은 앞부분을 생략(CAN-SKIP-UNTIL과 함께) |
PART-HOLD-BACK | 라이브 엣지에서 최소한 얼마나 뒤에서 재생할지(파트 사용 시) |
정리하면 HLS 저지연의 기준 문서는 기본 RFC 8216과, 사용하는 도구·클라이언트가 구현한 LL-HLS 사양(Apple 문서, 8216bis 초안 이력)을 함께 봐야 합니다.
최적화
세그먼트 길이
- 짧게(1~2초): 라이브 지연과 채널 전환 시간이 줄고 ABR이 빨리 반응합니다. 대신 요청 수와 플레이리스트 갱신 빈도가 늘고, 짧은 GOP 때문에 같은 화질에서 비트레이트가 올라갑니다.
- 길게(6~10초): 요청 수와 오리진 부하가 줄고 캐시 효율이 좋아집니다. 라이브 지연과 채널 전환 대기 시간은 늘어납니다. Apple 저작 가이드는 6초를 권장합니다.
인코딩 GOP의 정수배로 세그먼트 길이를 맞추지 않으면, 세그먼트가 짧을수록 길이 불일치와 끊김이 더 잘 드러납니다.
플레이리스트 길이(슬라이딩 윈도)
RFC 8216은 클라이언트가 라이브 플레이리스트 끝에서 타깃 길이의 3배 이내에 있는 세그먼트부터 재생을 시작하지 않도록 권고합니다. 그래서 라이브 플레이리스트에는 적어도 타깃 길이 3배 이상의 세그먼트가 남아 있어야 합니다. hls_list_size를 너무 작게 잡으면 네트워크 지연이나 재시도 중에 플레이리스트에서 이미 사라진 세그먼트를 요청하는 일이 생기고, 너무 크게 잡으면 플레이리스트 파일이 커져 매번 받는 비용이 늘어납니다. 플레이리스트에서 빠진 세그먼트도 한동안 서버에 남겨 두어야 늦게 도착한 클라이언트가 404를 받지 않습니다.
Prefetch 전략
- hls.js: 버퍼 길이 설정(
maxBufferLength등), 플레이어 크기에 맞춰 화질 상한을 두는capLevelToPlayerSize같은 옵션으로 불필요한 선행 다운로드를 조절합니다. 설정 이름은 버전별로 확인하십시오. - CDN: 프리로드 힌트로 미리 요청하는 URL에도 쿠키·토큰·CORS 정책이 똑같이 적용되는지 확인하십시오. 미리 보낸 요청이 401/403을 받으면 오히려 재생이 늦어집니다.
버퍼링 최소화(균형)
BANDWIDTH와AVERAGE-BANDWIDTH를 실제 값에 맞춰야 ABR이 필요 이상으로 낮은 화질에 머무르지 않습니다.- DNS 조회, TLS 핸드셰이크, TTFB 같은 네트워크 지연은 HLS 설정만으로는 줄일 수 없습니다. 엣지 배치, HTTP/2·HTTP/3 사용 여부처럼 전송 계층에서 다룰 문제입니다.
- 오리진과 CDN이
m3u8에 대해ETag/If-None-Match조건부 요청과 304 응답을 지원하는지 확인하면 변화 없는 플레이리스트를 다시 받는 비용을 줄일 수 있습니다.
브라우저 구현
hls.js (MSE)
hls.js는 MSE(Media Source Extensions)를 이용해 JavaScript로 HLS를 재생합니다. 매니페스트를 파싱하고 세그먼트·파트를 받아 SourceBuffer에 추가하는데, TS 세그먼트는 브라우저가 MSE로 받아 주지 않으므로 hls.js가 JavaScript에서 fMP4로 트랜스먹싱한 뒤 넣습니다. 지원 코덱은 브라우저와 플랫폼에 따라 다릅니다(공식 문서: hls.js README).
최소 예시(개념):
<video id="v" controls playsinline></video>
<script type="module">
import Hls from 'https://esm.sh/hls.js@1';
const video = document.getElementById('v');
const src = 'https://example.com/hls/master.m3u8';
if (Hls.isSupported()) {
const hls = new Hls({
// 예: ABR, 버퍼(버전/환경에 따라 키 이름 확인)
maxBufferLength: 30,
maxMaxBufferLength: 60
});
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
hls.on(Hls.Events.ERROR, (e, data) => {
if (data.fatal) {
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();
}
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src; // iOS 사파리 등(네이티브)
}
</script>
핵심 API·이벤트(버전 1.x 기준, 이름은 릴리스 노트로 확인):
Hls.isSupported(): MSE와 필요한 코덱을 쓸 수 있는지 확인loadSource/attachMediaMANIFEST_PARSED,LEVEL_SWITCHED,FRAG_LOADED등의 이벤트ERROR:fatal이면startLoad/recoverMediaError/destroy로 분기- 설정:
maxBufferLength계열,liveSyncDurationCount, 저지연용lowLatencyMode등 — hls.js 문서와 CHANGELOG를 확인하십시오
Video.js + HLS
Video.js 7 이상에는 VHS(videojs/http-streaming)가 내장되어 HLS와 DASH를 MSE로 재생합니다. VHS는 TS와 fMP4 세그먼트를 모두 지원하지만 기능별 지원 범위가 다르므로 지원 기능 문서로 필요한 조합을 확인하십시오.
VHS 기본(번들·타입·재생):
<link href="https://vjs.zencdn.net/8.10.0/video-js.min.css" rel="stylesheet" />
<video id="p" class="video-js" controls playsinline
data-setup='{"html5": {"vhs": {"overrideNative": true}}}'>
</video>
<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
<script>
const p = videojs('p', { controls: true });
p.src({ type: 'application/x-mpegURL', src: 'https://example.com/hls/master.m3u8' });
p.ready(() => p.play().catch(() => {}));
p.on('error', () => { /* VHS: network, manifest, mediasource 등 코드 분기 */ });
</script>
overrideNative는 브라우저의 네이티브 HLS 대신 VHS(MSE)로 재생할지를 정합니다. 여러 브라우저에서 같은 동작과 이벤트를 얻을 수 있지만, iPhone처럼 MSE가 없거나 제한된 환경에서는 동작하지 않고, 네이티브 재생보다 전력 소모가 크며 FairPlay 같은 네이티브 DRM 경로도 쓸 수 없습니다. 그래서 Safari에서는 네이티브를 쓰도록 브라우저별로 값을 다르게 주는 경우가 많습니다.
Safari 네이티브 HLS
macOS·iOS Safari와 AVPlayer 계열은 HLS를 네이티브로 재생하므로 MSE가 필요 없습니다. <video src="...m3u8">만으로 동작합니다(Apple 문서 참고).
- FairPlay: 웹에서는 Safari의 EME(
com.apple.fps)를 쓰고, 앱에서는AVContentKeySession을 씁니다. 키 요청 흐름이 달라 서버 쪽 처리도 나눠야 할 수 있습니다. - 인라인 재생: iOS에서 전체 화면으로 전환되지 않게 하려면
playsinline속성이 필요합니다. - CORS: 네이티브 재생은 브라우저가 직접 다운로드하므로 hls.js만큼 CORS에 민감하지 않지만, 인증 쿠키나 토큰 정책은 모든 요청에서 일관돼야 합니다.
에러 핸들링(공통)
| 유형 | 원인(예) | 대응 |
|---|---|---|
| 네트워크(404/0 byte) | 세그먼트 삭제, 캐시 오염, URL 만료 | 슬라이딩 윈도우와 세그먼트 보존 기간, 엣지 캐시와 오리진의 일치 여부 점검 |
| CORS | m3u8에만 헤더가 있고 세그먼트·키는 차단됨 | Access-Control-Allow-Origin을 모든 자산에 적용 |
| 401/403 | 토큰 만료 | 토큰 갱신, 매니페스트와 세그먼트 URL의 토큰 동기화 |
| MEDIA_ERR_DECODE | 코덱, fMP4 init, SPS 불일치 | CODECS 선언과 실제 인코딩 설정 통일 |
| 스톨(멈춤) | 버퍼 부족, GOP 불일치, 타임스탬프 점프 | ABR 설정, 키 회전 처리, EXT-X-DISCONTINUITY 표기 확인 |
hls.js에서는 치명적 오류가 나면 recoverMediaError나 destroy 후 재생성, 사용자 동작 이후 play() 재시도, 화질 상한 조정을 조합하는 패턴이 흔합니다.
같이 보면 좋은 글
- CMAF(Common Media Application Format) 레퍼런스 — ISO 23000-19·fMP4·HLS+DASH·저지연
- RTMP·RTSP·HLS·DASH·CMAF·WebRTC 비교
- AAC 오디오 코덱
- MP3 오디오 코덱 실전 활용 | LAME·CBR·VBR·FFmpeg 인코딩 가이드