joooii

[107일차] HLS 본문

TIL

[107일차] HLS

joooii 2026. 2. 10. 00:16

 

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

 

HLS란?

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

 

https://velog.io/@zizonyoungjun/HLS%EB%A1%9C-%EB%B9%84%EB%94%94%EC%98%A4-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0

 

HLS 특징

  • 미디어 콘텐츠를 작은 조각(청크) 단위로 전송
  • 네트워크 상태에 따라 적절한 품질의 비디오를 선택할 수 있게 설계
  • 방화벽 문제 최소화
  • 적응형 비트레이트 (ABR, Adaptive Bitrate) 지원 → 원활한 재생 환경 제공

 

HLS 장점

  • 모든 인터넷 연결 장치가 HTTP를 지원하기 때문에 전용 서버가 필요한 스트리밍 프로토콜보다 간단하게 실행 가능
  • 재생에 지장을 주지 않고, 네트워크 상태에 따라 비디오 품질을 높이거나 낮출 수 있음

 

기존 스트리밍 방식과의 차이점

 


m3u8

m3u8은 HLS가 미디어를 청크 단위로 전송하여 이 청크들을 관리 및 재생할 수 있도록 하는 플레이리스트 파일이다.

HLS에서는 거의 표준처럼 사용하는 파일이다 (JSON, XML도 가능하긴 함). 해당 이유는 다음과 같다.

  • 단순한 텍스트 기반 포맷
    • 사람이 읽고 수정하기 용이
    • 새로운 해상도나 비트레이트를 쉽게 추가 가능
  • 광범위한 지원
    • 대부분의 플레이어 및 스트리밍 서비스에서 기본적으로 지원
  • CDN 및 캐싱 친화적
    • HTTP 기반 m3u8 파일은 정적 파일처럼 다룰 수 있어서 효율적인 배포 가능

 

https://americanopeople.tistory.com/336

 

 

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 동작 흐름

https://duck-blog.vercel.app/blog/web/understand-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 자체에 서명과 만료 시간을 포함시키세요.
  • 원인 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를 사용하면서 이미지를 업로드 과정을 구현하면서 영상 쪽도 해보고 싶었다. 영상 쪽 공부를 열심히 해서 이번 프로젝트에서 잘 살려봐야겠다.. 

 

https://velog.io/@zizonyoungjun/HLS%EB%A1%9C-%EB%B9%84%EB%94%94%EC%98%A4-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0