08 — Player Engines (hls.js / Shaka / Video.js / ExoPlayer / AVPlayer)
이 문서가 답하는 질문: HLS·DASH·DRM 표준을 실제로 처리하는 코드는 무엇이고, 어떤 엔진을 어디에 써야 하나? 핵심 한 줄: 플레이어 엔진은 매니페스트를 읽고, ABR을 결정하고, MSE/EME를 운전하는 클라이언트 측 OS다. 디바이스마다 기본 엔진이 다르고, 지원 표준이 다르다.
한 줄 답 (Pyramid Top)
플레이어 엔진은 다섯 가지 — hls.js(Web HLS), Shaka Player(Web 통합), Video.js(UI shell), ExoPlayer/Media3(Android), AVPlayer(iOS). 어느 디바이스에서 무엇을 받느냐가 엔진 선택을 결정한다 — 자유롭게 고르는 게 아니다.
Why — 왜 플레이어 엔진이 별도 레이어인가
브라우저의 <video src="...">는 progressive download와 iOS의 native HLS만 지원한다. 그 외:
- DASH — 브라우저가 처음부터 지원 안 함.
- HLS on Chrome/Firefox — 마찬가지로 native 미지원.
- DRM — EME API는 있지만 각 DRM별 메시지 처리는 라이브러리가 필요.
- ABR — 자동 비트레이트 전환 알고리즘은 플레이어가 구현.
→ 그래서 플레이어 엔진이 MSE + EME + 매니페스트 파서 + ABR + 자막 + thumbnail을 합쳐서 하나의 추상화로 노출.
How — Web 엔진 비교
| 축 | hls.js | Shaka Player | Video.js |
|---|---|---|---|
| 만든 이 | Dailymotion/커뮤니티 | Brightcove | |
| HLS | ✅ (전문) | ✅ | (플러그인 필요) |
| DASH | ❌ | ✅ | (플러그인 필요) |
| MSS (Smooth Streaming) | ❌ | ✅ | (플러그인) |
| DRM | EME 직접 (제한적) | ✅ Widevine/FairPlay/PlayReady | ✅ (플러그인) |
| LL-HLS | ✅ (1.x+) | ✅ (4.x+) | ✅ (플러그인 따라) |
| 번들 크기 | ~150KB | ~400KB | ~250KB (코어) |
| 위치 | 엔진 only | 엔진 only | UI shell + 엔진 wrapper |
| iOS Safari | iPad에서 가능, iPhone은 native HLS fallback | 동일 | 동일 |
| typical 사용 | HLS만 필요 + 가벼움 | DASH 또는 multi-DRM 필요 | 빠른 UI 구축 + 광범위 호환 |
1) hls.js
가벼움 + HLS 전문. MSE에 fMP4/TS를 demux해 append하는 일이 핵심.
import Hls from 'hls.js';
if (Hls.isSupported()) {
const video = document.querySelector('video');
const hls = new Hls({
abrEwmaFastLive: 3.0,
abrEwmaSlowLive: 9.0,
abrBandWidthFactor: 0.95,
abrBandWidthUpFactor: 0.7,
maxBufferLength: 30,
maxMaxBufferLength: 60,
enableWorker: true,
lowLatencyMode: true,
backBufferLength: 30,
});
hls.loadSource('https://cdn.example.com/master.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => {
console.log('quality →', data.level);
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// iOS native HLS
video.src = 'https://cdn.example.com/master.m3u8';
}전형 패턴: 지원되면 hls.js, 아니면 native. iOS Safari는 항상 후자로 떨어진다 (iPhone Safari).
강점
- HLS만 필요할 때 최소 비용 솔루션.
- worker로 main thread 부담 ↓.
- LL-HLS 1.x부터 안정.
약점
- DASH 안 함 → 두 매니페스트 운영하면 결국 Shaka 추가.
- DRM은 Widevine만 잘 됨. FairPlay/PlayReady는 setup 복잡.
2) Shaka Player
Google의 통합 엔진. DASH·HLS·MSS 다 다루고 DRM 3종 모두 잘 지원.
import shaka from 'shaka-player';
shaka.polyfill.installAll();
const video = document.querySelector('video');
const player = new shaka.Player(video);
player.configure({
abr: {
enabled: true,
bandwidthUpgradeTarget: 0.85,
bandwidthDowngradeTarget: 0.95,
},
streaming: {
bufferingGoal: 10,
rebufferingGoal: 2,
bufferBehind: 30,
},
drm: {
servers: {
'com.widevine.alpha': 'https://wv.example.com/license',
'com.apple.fps.1_0': 'https://fps.example.com/license',
},
},
});
player.addEventListener('error', (e) => console.error(e.detail));
await player.load('https://cdn.example.com/manifest.mpd');
// 또는
await player.load('https://cdn.example.com/master.m3u8');강점
- HLS와 DASH 모두. 매니페스트 형식만 바꾸면 같은 코드.
- DRM 3종 모두 공식 지원.
- LL-HLS, LL-DASH 모두.
- Subtitle / thumbnail track / WebVTT thumbnails / multi-audio.
- 큰 OTT가 검증.
약점
- 번들 크기 (~400KB).
- 자체 UI는 기본적이라 커스텀 UI 필요 시 직접 구현.
- API surface가 크다 — 학습 곡선.
shaka.ui.Overlay (기본 UI)
Shaka는 shaka.ui.Overlay로 기본 컨트롤을 자동 제공. Pause/Play, ABR menu, subtitle menu, fullscreen 등.
const ui = new shaka.ui.Overlay(player, container, video);
ui.configure({
controlPanelElements: ['play_pause', 'time_and_duration', 'spacer', 'mute', 'volume', 'fullscreen', 'overflow_menu'],
overflowMenuButtons: ['captions', 'quality', 'language', 'playback_rate'],
});3) Video.js
UI 중심. 엔진 부분은 플러그인에 위임.
import videojs from 'video.js';
import 'videojs-contrib-quality-levels';
import 'videojs-http-streaming'; // HLS/DASH 통합 엔진
const player = videojs('my-video', {
html5: {
vhs: {
overrideNative: true,
enableLowInitialPlaylist: true,
}
}
});
player.src({
src: 'https://cdn.example.com/master.m3u8',
type: 'application/x-mpegURL'
});강점
- 플러그인 생태계 — 분석, 광고, UI 테마 등 수백 개.
- UI customization이 쉽다 (테마, 스킨).
- 빠른 prototyping.
약점
- 엔진 자체 구현이 아니라 wrapper — DRM/LL 같은 엣지에서 플러그인 호환이 흔들린다.
- HLS/DASH 모두 잘 되려면 VHS(videojs-http-streaming) 플러그인 필요 — 그게 사실상 내부 엔진.
How — Native 엔진 (모바일/TV 앱)
웹이 아니라 앱이라면 엔진이 디바이스 native다.
4) ExoPlayer / Media3 (Android)
Google이 만든 Android의 de facto 미디어 엔진. AndroidX Media3로 통합 (2022~).
val player = ExoPlayer.Builder(context).build()
val mediaItem = MediaItem.fromUri("https://cdn.example.com/master.m3u8")
player.setMediaItem(mediaItem)
player.prepare()
player.play()// DRM
val drmConfig = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri("https://widevine.example.com/license")
.setLicenseRequestHeaders(mapOf("Authorization" to "Bearer $jwt"))
.build()
val mediaItem = MediaItem.Builder()
.setUri("https://cdn.example.com/manifest.mpd")
.setMimeType(MimeTypes.APPLICATION_MPD)
.setDrmConfiguration(drmConfig)
.build()강점
- HLS, DASH, MSS, ProgressiveMP4 모두.
- DRM은 MediaDrm API 직결 — Widevine L1 자연스럽게.
- Android의 모든 코덱(HEVC/AV1/VP9) hardware decode.
- 4K HDR 정상 동작.
특징
- Renderer-based 구조: AudioRenderer, VideoRenderer, TextRenderer가 각 트랙을 책임.
- DataSource 추상화로 캐싱·재시도·인증을 끼워넣기 쉬움.
- OfflineLicenseHelper로 다운로드 콘텐츠 라이선스 관리.
5) AVPlayer (iOS / macOS / tvOS)
iOS의 native 미디어 엔진. AVFoundation 프레임워크.
import AVKit
let url = URL(string: "https://cdn.example.com/master.m3u8")!
let player = AVPlayer(url: url)
let playerVC = AVPlayerViewController()
playerVC.player = player
present(playerVC, animated: true)
player.play()// FairPlay
class ContentKeyDelegate: NSObject, AVContentKeyDelegate {
func contentKeySession(_ session: AVContentKeySession,
didProvide keyRequest: AVContentKeyRequest) {
// 1. App Cert 받기
// 2. SPC 만들기
keyRequest.makeStreamingContentKeyRequestData(forApp: appCert,
contentIdentifier: contentId.data(using: .utf8),
options: nil) { spc, error in
// 3. 라이선스 서버에 SPC 보내기
// 4. CKC 받아 keyRequest.processContentKeyResponse(...)
}
}
}강점
- HLS native — 매니페스트가 미디어로 그냥 받아짐.
- FairPlay 통합 — Secure Enclave에서 직접 키 처리.
- HDR / Dolby Atmos / spatial audio 모두 시스템 통합.
- AirPlay / picture-in-picture 자동.
약점
- DASH 미지원 — iOS 앱에서 DASH 하려면 별도 엔진(Shaka iOS 시도 또는 자체 구현).
- 디버깅 도구 부족 — 매니페스트 파싱 에러를 상세히 알기 어렵다.
- API가 imperative하고 KVO에 많이 의존 (SwiftUI보다는 UIKit 친화).
What — 한 OTT 서비스가 실제로 가져가는 조합
→ HLS + DASH 두 매니페스트를 항상 만든다. CMAF로 segment 공유하니 비용은 거의 안 든다.
What — ABR 알고리즘의 차이 (엔진별)
| 엔진 | 기본 ABR | 튜닝 |
|---|---|---|
| hls.js | EWMA fast/slow의 최솟값, throughput-based | abrEwma*, abrBandWidth*Factor |
| Shaka | 단순 throughput + 히스테리시스 | abr.bandwidthUpgrade/Downgrade Target |
| Video.js (VHS) | hls.js 기반 | 부분 노출 |
| ExoPlayer | DefaultLoadControl + DefaultBandwidthMeter | LoadControl, TrackSelectionParameters |
| AVPlayer | 블랙박스 — Apple 알고리즘 | preferredPeakBitRate, preferredMaximumResolution |
→ AVPlayer는 ABR 알고리즘을 튜닝 못한다. 상한만 줄 수 있다 (preferredPeakBitRate=2_000_000). 그래서 라이브 OTT가 Apple TV에서 의도치 않은 4K로 폭주하는 사고가 있을 수 있다 — 항상 cap을 명시.
What — 엔진 별 LL-HLS 지원 (2024~ 기준)
| 엔진 | LL-HLS | 비고 |
|---|---|---|
| AVPlayer (iOS 14+) | ✅ Native | partial segment, blocking GET 모두 |
| Shaka 4.x+ | ✅ | configure로 활성화 |
| hls.js 1.x+ | ✅ | lowLatencyMode: true |
| ExoPlayer | ✅ | Media3 1.0+ |
| Video.js (VHS) | 부분적 | VHS 버전에 따라 |
What-if — 잘못 고르면 어떻게 깨지는가
| 증상 | 원인 | 처방 |
|---|---|---|
| Android 앱에서 DASH 안 받힘 | DASH MIME 미명시 | MimeTypes.APPLICATION_MPD 명시 |
| iOS 앱에서 4K가 안 나옴 | preferredMaximumResolution 작음 | (width, height) 충분히 |
| Chrome에서 HLS만 만들고 hls.js 없음 | hls.js 미탑재 | hls.js 또는 Shaka 추가 |
| iPhone Safari에서 자막이 깨짐 | WebVTT 형식 오류 | iOS HLS의 자막은 segmented WebVTT만 |
| AVPlayer에서 DRM 라이선스 폭주 | 세션 단위로 매번 요청 | persistent license + KeyCache |
| Shaka에서 buffer가 너무 짧음 | bufferingGoal=2 같이 너무 낮음 | 8~15초 |
| ExoPlayer에서 ABR이 4K로 시작 | TrackSelectionParameters 미설정 | maxVideoSize 설정 |
| 모바일 데이터에서 4K 자동 시작 | 네트워크 타입 감지 안 함 | NetworkInfo 보고 cap 동적 |
Insight — 흥미로운 이야기
“Shaka는 사실 YouTube의 내부 엔진이 아니다”
YouTube는 자체 플레이어를 쓴다 (오픈소스 아님). Shaka는 Google이 Chromecast 등을 위해 별도로 만든 OSS. 두 시스템은 코드 공유가 거의 없다 — 같은 회사인 것이 우연.
“hls.js의 1.0이 늦게 나온 이유”
hls.js는 2015년에 시작했지만 1.0은 2022년에야 나왔다. 그 사이 LL-HLS 표준이 굳어지고, fMP4 HLS가 보편화되고, DRM이 까다로워지면서 0.x 시리즈에서 계속 진화했다. 1.0은 LL-HLS 안정 + fMP4 안정의 신호.
“AVPlayer가 ABR 알고리즘을 노출 안 하는 이유”
Apple은 ABR을 시스템 자원 최적화의 일부로 본다. 다른 앱이 background에서 다운로드 중이면 같은 디바이스의 AVPlayer가 덜 공격적으로 동작해야 한다 → OS가 알아서. 개발자가 튜닝하면 그 시스템적 균형이 깨진다는 철학. 그 결과 예측 가능성은 낮아진다 — 같은 콘텐츠가 같은 디바이스에서도 매번 다른 순서로 quality 전환할 수 있다.
“ExoPlayer가 Media3로 이름을 바꾼 이유”
이름이 비디오 only처럼 들렸다. Media3는 Audio + Video + UI + Cast까지 통합 SDK. 호환성: ExoPlayer 코드는 거의 그대로 Media3로 마이그레이션 가능 — 패키지명만 변경.
“한 회사가 자체 엔진을 만드는 경우”
Netflix, YouTube, Disney+, Amazon Prime Video 등은 자체 엔진을 가진다. 이유: (a) 자기 콘텐츠에 fully customize (P2P CDN, 자체 ABR, 자체 분석), (b) 디바이스별로 극한 최적화, (c) 라이선스 정책 통합. 일반 OTT는 Shaka/ExoPlayer/AVPlayer로 충분.
한 단락 요약 + Mermaid
플레이어 엔진은 디바이스마다 정해져 있다 — 자유 선택이 아니다. Web은 hls.js(HLS) 또는 Shaka(통합), iOS/macOS는 AVPlayer(HLS+FairPlay), Android는 ExoPlayer/Media3, Smart TV는 디바이스 native + Shaka fallback. 한 OTT는 항상 HLS + DASH 두 매니페스트를 만들고 CMAF로 segment 공유해 모든 엔진을 커버한다. ABR 알고리즘과 LL 지원 정도가 엔진 선택의 세부 변수다.
챕터 마감
이로써 06-streaming의 8개 단원이 끝났다 —
01-abr-theory.md— 비트레이트는 왜 바뀌나02-hls.md— Apple의 텍스트 매니페스트03-mpeg-dash.md— ISO의 XML 트리04-cmaf.md— 두 표준의 segment 통합05-low-latency.md— 지연 30s → 2s06-segmentation-and-manifest.md— segment 길이와 GOP 정렬07-drm.md— 누가 키를 풀어주는가08-player-engines.md— 누가 그 모두를 처리하는가
→ 다음 챕터(07-pipeline-theory): 이 모든 것을 자동 생성하는 변환 파이프라인.