| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- 나눔스퀘어
- 종합프로젝트
- 유레카 부트캠프 프론트엔드
- React
- 멀티캠퍼스부트캠프
- 유플텍플
- streaming metadata
- 멀티캠퍼스it부트캠프
- LG유플러스 유레카 프론트엔드 대면
- 입력처리방식
- 프론트엔드
- 핏로그
- 웹시큐리티
- 이미지 파일 관리
- 마이배티스
- 유레카프론트엔드대면
- LG U+
- 유레카프론트엔드
- jandi
- input="password"
- 미니프로젝트
- 부트캠프후기
- 상태관리
- 엘지유플러스프론트엔드대면
- sentry
- 멀티캠퍼스 부트캠프
- 타입스크립트
- 유레카 프론트엔드
- 유레카 프론트엔드 대면
- 유레카
- Today
- Total
joooii
[107일차] HLS 본문

이번 융합프로젝트 주제인 OTT를 구현하기 위해 HLS를 찾아보았다.

HLS란?
HLS (HTTP Live Streaming)은 Apple에서 개발한 적응형 스트리밍 프로토콜이며, HTTP 기반이다.

HLS 특징
- 미디어 콘텐츠를 작은 조각(청크) 단위로 전송
- 네트워크 상태에 따라 적절한 품질의 비디오를 선택할 수 있게 설계
- 방화벽 문제 최소화
- 적응형 비트레이트 (ABR, Adaptive Bitrate) 지원 → 원활한 재생 환경 제공
HLS 장점
- 모든 인터넷 연결 장치가 HTTP를 지원하기 때문에 전용 서버가 필요한 스트리밍 프로토콜보다 간단하게 실행 가능
- 재생에 지장을 주지 않고, 네트워크 상태에 따라 비디오 품질을 높이거나 낮출 수 있음
기존 스트리밍 방식과의 차이점

m3u8
m3u8은 HLS가 미디어를 청크 단위로 전송하여 이 청크들을 관리 및 재생할 수 있도록 하는 플레이리스트 파일이다.
HLS에서는 거의 표준처럼 사용하는 파일이다 (JSON, XML도 가능하긴 함). 해당 이유는 다음과 같다.
- 단순한 텍스트 기반 포맷
- 사람이 읽고 수정하기 용이
- 새로운 해상도나 비트레이트를 쉽게 추가 가능
- 광범위한 지원
- 대부분의 플레이어 및 스트리밍 서비스에서 기본적으로 지원
- CDN 및 캐싱 친화적
- HTTP 기반 m3u8 파일은 정적 파일처럼 다룰 수 있어서 효율적인 배포 가능

HLS 주요 포맷과 m3u8 파일 구조
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
https://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=1280x720
https://example.com/mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1920x1080
https://example.com/high/index.m3u8
코드 설명
#EXTM3U
- m3u8 파일의 시작을 알리는 필수 헤더, 이 줄이 없으면 플레이어가 HLS라 인식 X
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
https://example.com/low/index.m3u8
- 하나의 화질(Variant Stream)을 정의하는 블록
- BANDWIDTH=800000
- 초당 비트 전송량 (bps)
- 800kbps
- 플레이어가 네트워크 상황 판단할 때 사용
- RESOLUTION=640x360
- 영상 해상도
- UI에서 "360p / 720p / 1080p" 같은 선택지로 쓰임
- url
- 해당 화질의 실제 미디어 플레이리스트
- `.ts`, `.m4s` 조각 목록이 들어있음
- 규칙: #EXT-X-STREAM-INF 다음 줄은 반드시 해당 스트림의 m3u8 url
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=1280x720
https://example.com/mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1920x1080
https://example.com/high/index.m3u8
- 360p → `low/index.m3u8`
- 720p → `mid/index.m3u8`
- 1080p → `high/index.m3u8`
전체 구조
Master Playlist (이 파일)
├─ low/index.m3u8 → 360p 세그먼트 목록
├─ mid/index.m3u8 → 720p 세그먼트 목록
└─ high/index.m3u8 → 1080p 세그먼트 목록
종류별 유형
1. 마스터 플레이리스트: 재생 가능한 모든 화질 옵션을 나열한 파일 (client가 가장 먼저 요청하는 파일)
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.4d401f,mp4a.40.2"
quality_360p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
quality_720p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
quality_1080p/playlist.m3u8
2. 미디어 플레이리스트: 실제 재생할 세그먼트 파일들의 목록. 마스터 플레이리스트에서 특정 화질을 선택하면 해당 미디어 플레이리스트를 요청
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="https://key-server.example.com/key?track=audio",IV=0x1234567890ABCDEF
#EXTINF:10.0,
segment_001.ts
#EXTINF:10.0,
segment_002.ts
#EXTINF:10.0,
segment_003.ts
#EXT-X-ENDLIST
HLS 동작 흐름

추가 지식
`.ts`
- 영상 청크 단위
- MPEG-TS(MPEG Transport Stream) 라는 영상 컨테이너 포맷
`m4s`
- CMAF
- 화질 효율 더 좋음
- ts와 같이 청크 단위
🚨 Safari에서 MSE 방식 사용 및 iOS 특정 버전에서 발생하는 문제 🚨
HLS 스트리밍 이해하기: 적응형 비트레이트와 m3u8 파일 설명
HLS의 개념과 장점, 기존 스트리밍 방식과의 차이점, Safari에서 기본 지원하는 이유, 다른 브라우저에서 사용하는 방법, m3u8 파일 구조 및 예제 코드까지 모두 정리했습니다. 또한 iOS 특정 버전에
duck-blog.vercel.app
왜 청크로 쪼개서 전송할까?
- 세그먼트는 보통 2~10초 단위로 나뉨 (음원 스트리밍은 10초)
- 이점
- 빠른 시작: 첫 번째 조각만 받으면 바로 재생 시작
- 화질 전환: 네트워크 상태에 따라 다음 조각부터 화질 변경 가능
- 보안: 전체 파일을 한 번에 탈취 불가
- 효율성: 필요한 만큼만 버퍼링
클라이언트 구현 및 라이브러리
주요 라이브러리
- hls.js → 일반적 서비스라면 가장 합리적
- 가장 널리 쓰이는 가벼운 라이브러리
- HLS 전용
- 커스터마이징 용이 (넷플, 트위치 등 사용)
- Video.js
- 강력한 UI와 플러그인 생태계를 가진 전통 강자
- 내부 엔진으로 hls.js 사용 가능
- Shaka Player
- 구글이 만든 라이브러리
- DASH, HLS 모두 지원
- 오프라인 저장 및 멀티 DRM 처리에 강점
hls.js
- 브라우저 내부에서 Transmuxing (TS → MP4 실시간 변환) 처리
- 기본적으로 TS 재생을 지원하지 않는 브라우저에서도 재생이 가능하게 함
- React 코드 예시
import { useEffect, useRef, useState } from 'react'
import Hls from 'hls.js'
interface HlsPlayerProps {
src: string
autoPlay?: boolean
}
export function HlsPlayer({ src, autoPlay = false }: HlsPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
const video = videoRef.current
if (!video) return
// 1순위: hls.js 지원 여부 먼저 확인 (Android, PC 등)
// 중요: 안드로이드 크롬도 canPlayType이 true지만, 기능이 부족한 네이티브 대신 hls.js를 우선 써야 함
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
// 주의: iOS Safari(네이티브)는 이 설정을 무시함.
// iOS 대응을 위해선 토큰을 헤더가 아닌 '쿼리 스트링'이나 '쿠키'로 넘겨야 함.
xhrSetup: (xhr, url) => {
if (url.includes('keyserver')) {
xhr.setRequestHeader('Authorization', `Bearer ${getToken()}`)
}
},
})
hls.loadSource(src)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (autoPlay) video.play().catch(console.error)
})
// 실무 팁: 토큰 만료(401) 시 재발급 및 재시도 로직
hls.on(Hls.Events.ERROR, async (event, data) => {
if (data.response?.code === 401) {
console.warn('토큰 만료 감지. 갱신 로직 실행')
// 1. 새 토큰 발급: const newToken = await refreshToken()
// 2. hls 설정 갱신: hls.config.xhrSetup = ...
// 3. 복구 시도: hls.recoverMediaError()
}
})
return () => hls.destroy()
}
// 2순위: hls.js 미지원 시 네이티브 플레이어 사용 (주로 iOS Safari)
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src
// iOS는 'Authorization' 헤더 커스텀이 불가능합니다.
// 따라서 src URL 자체에 ?token=xxx 같이 인증 정보를 포함해야 합니다 (Presigned URL 추천).
}
}, [src, autoPlay])
// playsInline: iOS에서 영상 재생 시 강제로 전체화면이 되는 것을 방지 (커스텀 UI 유지 필수)
return <video ref={videoRef} controls className="w-full rounded-lg" playsInline />
}
문제 발생 시, 원인 파악법
1. 문제 격리
"내 프론트엔드 코드 문제인가? 스트림 URL 자체 문제인가?"
- hls.js 데모 페이지 활용: hls.js 데모 페이지에 문제의 m3u8 주소를 넣어봅니다.
- 거기서도 안 됨: 소스(인코딩, CDN, 키서버) 문제 → 백엔드/미디어 팀 리포트
- 거기선 잘 됨: 내 프론트엔드 코드(설정, 라이프사이클) 문제
2. 증상별 체크리스트
장시간 재생 시 끊김 (Token Expiry) → 주로 토큰 관련
- 증상: 1시간 이상 긴 영상이나 플레이리스트 재생 중 뜬금없이 멈춤.
- 원인: 초기 로딩 시 받아온 인증 토큰이 만료(Expired)되어, 다음 세그먼트나 키 요청 시 401 에러 발생.
- 해결: `hls.on(Hls.Events.ERROR)`에서 `response.code === 401`을 감지하여 토큰을 갱신(Refresh)하고 재요청하는 로직 필수 구현.
무한 로딩 (Infinite Loading)
- 증상: 스피너만 돌고 시작 안 됨.
- 체크:
- CORS: 개발자 도구 콘솔의 빨간 CORS 에러 확인. m3u8, ts, 키서버 도메인 모두 허용 필요.
- Mixed Content: HTTPS 페이지에서 HTTP 스트림 요청 시 차단됨.
멈춤 (Stalling)
- 증상: 잘 나오다가 특정 구간에서 멈춤.
- 체크:
- Gap: 세그먼트 번호가 중간에 비거나 누락되었는지 확인.
- PTS (Presentation Time Stamp): 오디오/비디오 싱크가 틀어지면 hls.js가 맞추려다 멈출 수 있음.
- Discontinuity: 광고 삽입 등으로 속성이 변할 때 `#EXT-X-DISCONTINUITY` 태그 유무 확인.
PC에선 되는데 iOS에서 안 됨 (가장 흔한 이슈)
- 원인 1: 헤더 인증 불가 (Header Auth Fail)문제: iOS(Safari)의 네이티브 플레이어는 개발자가 HTTP 요청 헤더를 조작할 수 없습니다 (`Authorization` 헤더 추가 불가).
- 해결: 보안 토큰을 쿼리 파라미터(Query String)이나 쿠키(Cookie) 로 넘겨야 합니다.
- 추천: AWS CloudFront Signed URL 같은 Presigned URL 방식을 사용하여 URL 자체에 서명과 만료 시간을 포함시키세요.
- 해결: 보안 토큰을 쿼리 파라미터(Query String)이나 쿠키(Cookie) 로 넘겨야 합니다.
- 원인 2: 코덱 및 스펙 불일치증상: 재생 버튼 눌러도 무반응이거나 검은 화면.
- 체크:
- Codec Profile: 구형 iOS는 High Profile 미지원 가능성 (Main Profile 권장).
- Frame Rate: 가변 프레임 레이트(VFR)보다 고정 프레임 레이트(CFR) 권장.
- 해결: Mac의 `mediastreamvalidator` 툴로 스트림 스펙 위반 검사.
- 체크:
3. 키 관리 주의사항 (Key Rotation)
보안을 위해 키를 자주 교체(Rotation)할 경우, 클라이언트가 예전 키를 캐싱하고 있으면 재생 실패가 발생합니다.
- 해결: 키 요청 API 응답 헤더에 `Cache-Control: no-cache, no-store` 필수 명시.
- 클라이언트: `xhrSetup`이나 쿼리 파라미터로 캐시 방지 처리.
회고
저번 프로젝트에서 S3를 사용하면서 이미지를 업로드 과정을 구현하면서 영상 쪽도 해보고 싶었다. 영상 쪽 공부를 열심히 해서 이번 프로젝트에서 잘 살려봐야겠다..

'TIL' 카테고리의 다른 글
| [119일차] 프론트엔드 FSD 아키텍처 (0) | 2026.02.28 |
|---|---|
| [99일차] React 공식문서 톺아보기 - 1. UI 표현하기 (1) | 2026.01.27 |
| [95일차] STT와 TTS (0) | 2026.01.20 |
| [90일차] Jira 슬기롭게 사용하기 (0) | 2026.01.13 |
| [85일차] 애자일과 JIRA (0) | 2026.01.06 |
