Hls.js 찍먹: 브라우저 m3u8 재생까지

Lui.Slki·2026년 2월 19일

개발 성장일지

목록 보기
4/6
post-thumbnail

Hls.js란,

HLS(HTTP Live Streaming)

애플이 2009년 공개한 HTTP 기반 적응형 비트레이트(ABR) 스트리밍 방식이다.
특별한 전용 프로토콜이 아니라 그냥 HTTP로 파일을받아 재생하는 구조라서 웹/모바일/미디어서버에서 널리 지원되고 지금도가장 대중적인 스트리밍 포맷 중 하나다.

HLS의 행심 아이디어는 영상 한 덩어리를 내려받는 대신,

  • m3u8(플레이리스트) 파일을 먼저 받고
  • 그 안에 적힌 조각(segment) 들을 순서대로 요청해서 재생하는 스트리밍 방식

즉, "재생목록(m3u8)을 따라가면서 segment들을 계속 가져와 재생하는 방식" 이라서

  • 네트워크가 느려지면 낮은 화질 세그먼트로 바꾸고(ABR)
  • 다시 빨라지면 높은 화질로 복귀하는 식의 끊김 최소화가 가능하다.

참고로 master.m3u8에는 여러 화질(variant)이 들어갈 수 있고,
플레이어는 상황에 따라 적절한 variant를 선택해 재생한다.

그래서 왜 필요한가?

Safari 는 HLS를 네이티브로 재생할 수 있지만(브라우저가 m3u8을 직접 이해함)
Chrome/Edge/Firefox 는 기본 지원이 약해서 hls.js가 m3u8을 파싱하고 세그먼트를 받아 MSE(Media Source Extensions)로 video에 붙여 재생하게 된다.

  • Js가 m3u8을 파싱
  • 세그먼트 다운로드
  • 디코딩 가능한 형태로 이어 붙여
  • <video> 에 주입해야함

해당 과정을 구현해주는 라이브러리가 hls.js 이다.

그냥 mp4 를 내려받니 않고 HLS 를 쓰는 이유

라이브/스트리밍에서 HLS를 쓰는 이유는
1. 끊김을 줄이고
2. 네트워크에 맞춰 품질을 자동으로 바꾸고
3. 배포/운영을 쉽게 만들기 때문.


HLS는 "끊김 적게 + 빠른 시작 + 라이브에 최적 + CDN 운영 쉬움 + 품질/트랙 확장 쉬움" 때문에 사용한다.
그리고 그걸 브라우저에서 재생하려면 Safari는 네이티브로 되고, 나머지는 hls.js(MSE)가 필요하다.


hls.js 기본 사용 흐름

hls.js로 재생하는 기본 패턴은 거의 고정이다.
일단, hls.js 를 설치한다.

npm i hls.js
# 또는
yarn add hls.js
pnpm add hls.js

선택(있으면 편한것들)

  • video.js: 기본 컨트롤/스킨/플러그인 생태계
npm i video.js
# (TS 프로젝트면 타입도 보통 같이 오지만, 필요하면 @types/video.js 확인)

하지만 hls.js 학습용 미니 프로젝트 상황에서는 굳이 설치하지 않고 순수 <video> + 커스텀 버튼으로 가서 이해하도록 하겠다.

  • HLS 테스트용 로컬 서버 필요하면(m3u8/ts를 로컬에 두고 테스트 할 때 CORS/경로 문제 덜 겪게)
npm i -D serve
# 또는 vite dev server로 public 폴더에 두고 테스트 가능

현재 상황에서는 해당 선택 파트를 제외하고 학습을 진행하도록 할것이다.

  • Hls.isSupported() (MSE 되는 브라우저인지)
  • hls.loadSource(m3u8)
  • hls.attachMedia(videoEl)

Safari는 분기해서 네이티브로 처리한다.

import Hls from "hls.js";

function mountHls(videoEl, src) {
	// Safari: native HLS
  if (videoEl.canPlayType("application/vnd.apple.mpegurl")){
  	videoEl.src = src;
    return() => {videoEl.src="";};
  }
  
  // Others: Hls.js via MSE
  if (Hls.isSupported()) {
 	 const hls = new Hls({
       // 학습용 디버그.로깅하기 좋은 옵션들
     	enableWorker: true,
       	lowLatencyMode: true, 
     });
    
    hls.loadSource(src);
    hls.attachMedia(videoEl);
    
    hls.on(Hls.Events.MANIFEST_PARSED, () => {
    // autoplay는 브라우저 정책상 실패 가능성 있어서 사용자 제스처 필요
    	videoEl.play().catch(() => {});  
    });
    
    return () => hls.destroy();
  }
  
  console.warn("HLS가 해당 브라우저를 지원하지 않습니다.");
  return () => {};
}

화질(퀄리티) 선택하는 법

HLS에 여러 화질이 있으면, hls.levels에 들어온다.

  • 자동(ABR): hls.currentLevel = -1
  • 특정 화질 고정: hls.currentLevel = index
hls.on(Hls.Events.MANIFEST_PARSED, () => {
	console.log(hls.levels.map((l, i) => ({
    i,
      height: l.height,
      bitrate: l.bitrate,
    })));
});

// ex: 720p 고정
const idx = hls.levels.findIndex(l => l.height === 720);
if (idx >= 0) hls.currentLevel = idx;

// 자동 되돌리기
hls.currentLevel = -1;

Auto + 360/720/1080 드롭다운 정도만 있어도 학습상황에서 도움이 된다.

에러처리(복구) 패턴은 사실 정답이 있다.

hls.js는 에러 이벤트에서 fatal 여부를 준다

  • 네트워크 계열 fatal → hls.startLoad() 재시도
  • 미디어/디코딩 계열 fatal → hls.recoverMediaError()
  • 그 외 → destroy() 이후 새로 로드 (상단에 이미 사용한 흔적 있음)
hls.on(Hls.Events.ERROR, (event, data) => {
console.log("HLS error:", data.type, data.details, "fatal:", data.fatal);
  
  if (!data.fatal) return;
  
  switch (data.type) {
    case Hls.ErrorTypes.NETWORK_ERROR:
      hls.startLoad();
      break;
    case Hls.ErrorTypes.MEDIA_ERROR:
      hls.recoverMediaError();
      break;
    default:
      hls.destroy();
      
  }
})

디버깅은 해당 순서로 보면 빠르다

HLS 재생이 안된다 - 대부분 이쪽에서 걸림

1) m3u8이 네트워크로 받아지나?

  • DevTools → Network에서 *.m3u8 요청 확인

2) 세그먼트 요청이 이어지나?

  • .ts / .m4s 요청이 연달아 나가야 정상

3) CORS

  • 가장 흔한 함정
  • m3u8/세그먼트가 다른 도메인이면 CORS 헤더 필요

학습 시 목표 체크 리스트

  • m3u8 URL 입력 → 재생
  • Safari 네이티브 분기 처리
  • Quality(Auto/고정) 드롭다운
  • Error 상태 표시 + recover 동작 확인
  • 이벤트 로그/Network로 흐름 추적 가능

0개의 댓글