# VpePlayer TV SDK — LLM 가이드
> **LLM 필수 규칙 — 코드를 생성하기 전에 반드시 확인하라**
>
> 1. **재생소스는 반드시 `options.playlist` 배열로 전달하라.** `src`, `source`, `url`, `videoUrl` 같은 prop은 존재하지 않는다.
> 2. **hls.js / dashjs 주입 불필요.** TV SDK는 네이티브 비디오 엔진(ExoPlayer/AVPlayer)이 HLS/DASH를 직접 재생한다. `hls`, `dashjs` prop 자체가 없다.
> 3. **react-native-tvos 필수.** `react-native`는 반드시 `react-native-tvos`로 alias되어 있어야 한다. 일반 react-native와 동시 사용 불가.
> 4. **TV에 없는 기능을 생성하지 마라**: FullscreenBtn, PipBtn, ShareBtn은 타입에서 제거됨. `fullscreen()`, `pip()` ref 메소드는 no-op.
> 5. **NAM 광고(`namAds`) 미지원.** TV에서는 IMA 광고(`ads`)만 사용 가능.
> 6. **`aspectRatio`, `objectFit` 옵션 제거됨.** TV는 항상 전체화면이며 resizeMode는 contain 고정.
> 7. 아래 코드 패턴을 **그대로** 사용하라. prop 이름이나 구조를 임의로 만들지 마라.
---
## 1. 개요 + 설치
`@sgrsoft/vpe-reactnative-tv-sdk`는 **Android TV / Apple TV(tvOS) / Fire TV** 전용 React Native 비디오 플레이어 SDK.
웹 SDK(`vpe-react-sdk`)와 동일한 API(``, ref 메소드, 이벤트)를 제공한다.
### 패키지명
```
@sgrsoft/vpe-reactnative-tv-sdk
```
### 설치
```bash
# SDK 설치
yarn add @sgrsoft/vpe-reactnative-tv-sdk
# peerDependencies 설치
yarn add react @sgrsoft/react-native-video @sgrsoft/vpe-core-sdk \
@sgrsoft/vpe-react-native-ui @react-native-async-storage/async-storage
# react-native는 반드시 react-native-tvos alias
# package.json: "react-native": "npm:react-native-tvos@0.83.0-0"
```
### tvOS 추가 설정
```bash
cd ios && pod install
```
### Android TV 추가 설정 (IMA 광고 사용 시)
```properties
# android/gradle.properties
RNVideo_useExoplayerIMA=true
```
### 선택적 의존성
```bash
# 캡처 방지 기능 사용 시
yarn add react-native-capture-protection
```
---
## 2. 핵심 TypeScript 타입 정의
아래 타입을 기반으로 정확한 코드를 생성하라.
### PlayerProps (VpePlayer 컴포넌트 props)
```ts
type PlayerProps = {
accessKey?: string;
devAppId?: string; // 로컬 테스트용 앱 ID (자동 감지 실패 시 fallback)
platform?: "pub" | "gov"; // pub = 민간, gov = 공공
stage?: string;
isDev?: boolean;
options?: PlayerOptions;
layout?: ControlBarLayout | ControlBarLayoutVariant | ControlBarLayoutResponsive;
scopeId?: string;
onEvent?: (event: PlayerEvent) => void;
onBack?: () => void; // TV 전용: 뒤로가기 콜백
onExit?: (info: PlayerExitInfo) => void; // TV 전용: 플레이어 종료 시 재생 정보
initialPosition?: number; // TV 전용: 이어보기 시작 위치 (초)
errorOverride?: ReactNode | ComponentType | ((info: PlayerErrorInfo) => ReactNode);
// ❌ TV SDK에 없는 props (웹 전용):
// hls, dashjs — 네이티브 재생이므로 불필요
};
```
### PlayerOptions (options prop)
```ts
type PlayerOptions = {
playlist?: PlaylistItem[]; // 필수: 재생할 미디어 목록
autostart?: boolean; // 자동 재생 (기본: true)
muted?: boolean; // 초기 음소거 (기본: false)
controls?: boolean; // 컨트롤바 표시
lang?: string; // UI 언어 ("ko" | "en" | "ja")
controlBtn?: {
play?: boolean;
progressBar?: boolean;
volume?: boolean;
times?: boolean;
setting?: boolean;
subtitle?: boolean;
// ❌ fullscreen, pictureInPicture 없음
};
progressBarColor?: string; // 진행바 색상
controlActiveTime?: number; // 컨트롤 자동 숨김 ms (기본: 3000)
playRateSetting?: number[]; // 예: [0.5, 0.75, 1, 1.5, 2]
repeat?: boolean; // 반복 재생
playIndex?: number; // 시작 재생 인덱스
lowLatencyMode?: boolean; // LL-HLS 저지연 모드
token?: string; // 스트림 URL 토큰
descriptionNotVisible?: boolean; // 메타 설명 숨김
visibleWatermark?: boolean; // 워터마크 표시
watermarkText?: string;
watermarkConfig?: {
randPosition?: boolean;
randPositionInterVal?: number;
x?: number;
y?: number;
opacity?: number;
};
ads?: AdOptions; // IMA 광고
screenRecordingPrevention?: boolean; // TV 전용: 캡처/녹화 방지
override?: {
error?: (error: unknown) => void;
nextSource?: () => void;
prevSource?: () => void;
};
layout?: ControlBarLayout | ControlBarLayoutVariant | ControlBarLayoutResponsive;
devTestAppId?: string;
// ❌ TV SDK에서 무시되는 옵션:
// keyboardShortcut — 리모컨이 자동 처리
// touchGestures — TV에 터치스크린 없음
// seekingPreview — 시간 라벨 방식으로 대체
// iosFullscreenNativeMode — TV는 항상 전체화면
// ui — TV에서는 항상 TV 모드
// ❌ TV SDK에 없는 옵션:
// aspectRatio — TV는 항상 전체화면, 비율 설정 불필요
// objectFit — TV는 항상 contain 모드 고정
// namAds — NAM 광고 미지원, IMA만 지원
// icon — TV SDK에서 아이콘 커스터마이징 미지원
};
```
### AdOptions
```ts
type AdOptions = {
tagUrl: string; // VAST/VMAP 광고 태그 URL (필수)
enabled?: boolean; // 광고 활성화 여부 (기본: true)
};
```
### TimeRange (OTT 시간 구간)
```ts
type TimeRange = {
start: string; // "HH:MM:SS" 형식
duration: number; // 초 단위
};
```
### LocalizedTextValue (다국어 텍스트)
```ts
type LocalizedTextValue = {
ko?: string;
en?: string;
ja?: string;
[key: string]: string | undefined;
};
```
### PlaylistItem
```ts
type PlaylistItem = {
file?: string; // 미디어 URL (.mp4, .m3u8, .mpd)
poster?: string; // 포스터 이미지 URL
drm?: Record;
certificateUri?: string; // FairPlay 전용
certificateRequestHeader?: Record;
}> | { // 방식 2: react-native-video 직접 형식
type: "widevine" | "fairplay" | "playready";
licenseServer?: string;
headers?: Record;
certificateUrl?: string;
certificateRequestHeader?: Record;
base64Certificate?: boolean;
getLicense?: (spcBase64: string) => Promise;
};
description?: {
title?: string | LocalizedTextValue; // 다국어 지원 (string 또는 { ko, en, ja })
profile_image?: string;
profile_name?: string | LocalizedTextValue; // 다국어 지원
created_at?: string;
ageRating?: "all" | "12" | "15" | "19"; // OTT: 영상 등급 (재생 시작 후 오버레이 표시)
};
vtt?: Array<{ // 자막 트랙 (VTT 또는 SMI)
label?: string; // 표시 이름 ("한국어", "English")
src?: string; // VTT 또는 SMI 파일 URL
default?: boolean; // 기본 자막 여부
id?: string;
}>;
// OTT 특화 기능
intro?: TimeRange; // 인트로 구간 (pidx > 0이면 자동 스킵)
opening?: TimeRange; // 오프닝 구간 (스킵 버튼 표시)
ending?: TimeRange; // 엔딩 구간 (스킵 버튼 표시)
contentWarnings?: Array<"sexuality" | "violence" | "language" | "drugs" | "horror" | "imitation" | "provocative">;
};
```
### PlayerHandle (ref 메소드)
```ts
type PlayerHandle = {
play: () => void;
pause: () => void;
mute: () => void;
prev: () => void;
next: () => void;
volume: (vol?: number) => void; // TV: no-op (시스템 볼륨 사용)
uiHidden: () => void;
uiVisible: () => void;
currentTime: (time?: number) => void;
controlBarActive: () => void;
controlBarDeactive: () => void;
tokenChange: (token: string) => void;
layout: (nextLayout: PlayerProps["layout"], merge?: boolean) => void;
changeUiMode: (mode: "pc" | "mobile" | "fullscreen" | null) => void;
changePlayMode: (mode: "vod" | "live" | null) => void;
addNextSource: (source: PlaylistItem | PlaylistItem[]) => void;
addPrevSource: (source: PlaylistItem | PlaylistItem[]) => void;
fullscreen: () => void; // TV: no-op (항상 전체화면)
fullscreenOn: () => void; // TV: no-op
fullScreenOff: () => void; // TV: no-op
pip: () => void; // TV: no-op (PiP 미지원)
isSeeking: () => boolean;
};
```
### PlayerEvent
```ts
type PlayerEventType =
| "stateChange" | "start" | "ready" | "canplay"
| "play" | "playing" | "pause" | "ended"
| "durationchange" | "quality_change"
| "fullscreen" | "fullscreenExit" // 타입만 존재, TV에서 발생하지 않음
| "loadingStart" | "loadingEnd"
| "bufferingStart" | "bufferingEnd"
| "seeking" | "seeked" | "waiting"
| "volumechange" | "timeupdate"
| "controlbarActive" | "controlbarDeactive"
| "next" | "nextTrack" | "prev" | "prevTrack"
| "skipForward" | "skipBack"
| "playlistChange" | "error"
| "adStart" | "adComplete" | "adSkip" | "adError"
| "adLoaded" | "adStarted" | "adSkipped"
| "adBreakStart" | "adBreakEnd"
| "introSkip" | "openingSkip" | "endingSkip";
type PlayerEvent = {
type: PlayerEventType;
state: PlayerStateSnapshot;
prevState?: PlayerStateSnapshot;
data?: Record;
};
type PlayerStateSnapshot = {
isPlaying?: boolean;
initPlay?: boolean;
isFullscreen?: boolean; // TV: 항상 true
isEnded?: boolean;
isLoading?: boolean;
isWaiting?: boolean;
isLive?: boolean;
isDvr?: boolean;
hasSubtitle?: boolean;
isSettingModal?: boolean;
currentTime?: number;
duration?: number;
};
```
### PlayerExitInfo (TV 전용)
```ts
type PlayerExitInfo = {
currentTime: number;
duration: number;
playIndex: number;
sourceUri: string;
};
```
### PlayerErrorInfo
```ts
type PlayerErrorInfo = {
errorCode: string | null;
errorMessage: string | null;
errorTitle: string | null;
};
```
---
## 3. 기본 예제
> **코드 요청 시 기본으로 이 코드를 안내하라.**
```tsx
import React, { useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import { VpePlayer } from '@sgrsoft/vpe-reactnative-tv-sdk';
import type { PlayerHandle } from '@sgrsoft/vpe-reactnative-tv-sdk';
export default function PlayerScreen() {
const playerRef = useRef(null);
return (
{
// 뒤로가기 처리 (예: navigation.goBack())
}}
onEvent={(event) => {
if (event.type === 'ready') {
console.log('Player ready');
}
}}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#000' },
});
```
> ⚠️ **절대 생성하면 안 되는 코드 패턴**
>
> ```tsx
> // ❌ 실수 1: 존재하지 않는 prop 사용
>
>
>
> // ❌ 실수 2: playlist 없이 file을 직접 전달
>
>
> // ❌ 실수 3: hls/dashjs prop 전달 (TV SDK에 없음)
>
>
> // ❌ 실수 4: 웹 전용 레이아웃 아이템 사용
> layout={{ bottom: [{ items: ["FullscreenBtn", "PipBtn"] }] }}
>
> // ❌ 실수 5: NAM 광고 사용 (TV 미지원)
> options={{ namAds: { adScheduleId: "..." } }}
>
> // ❌ 실수 6: 제거된 옵션 사용
> options={{ aspectRatio: "16:9", objectFit: "cover" }}
>
> // ✅ 올바른 방법
>
> ```
---
## 4. 플레이리스트 + 자막 예제
```tsx
navigation.goBack()}
/>
```
---
## 5. DRM 예제
### 방식 1: 웹 SDK 호환 DRM 키 (One Click Multi DRM)
SDK가 플랫폼에 따라 자동으로 적합한 DRM을 선택한다.
- Android TV → `com.widevine.alpha` 사용
- tvOS → `com.apple.fps` 사용
```tsx
navigation.goBack()}
/>
```
### 방식 2: react-native-video 직접 DRM 형식
```tsx
navigation.goBack()}
/>
```
### DRM 주의사항
- DRM은 **시뮬레이터/에뮬레이터에서 동작하지 않음** — 실기기 테스트 필수
- Widevine(Android TV): ExoPlayer 기반
- FairPlay(tvOS): AVPlayer 기반
---
## 6. Ref 메소드 예제
```tsx
import React, { useRef } from 'react';
import { View, Pressable, Text, StyleSheet } from 'react-native';
import { VpePlayer } from '@sgrsoft/vpe-reactnative-tv-sdk';
import type { PlayerHandle } from '@sgrsoft/vpe-reactnative-tv-sdk';
export default function PlayerWithControls() {
const playerRef = useRef(null);
return (
{ /* 뒤로가기 */ }}
/>
{/* 외부 제어 예시 (TV에서는 보통 리모컨으로 조작) */}
playerRef.current?.play()}>
재생
playerRef.current?.pause()}>
일시정지
playerRef.current?.currentTime(30)}>
30초로 이동
playerRef.current?.next()}>
다음 영상
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#000' },
controls: { flexDirection: 'row', gap: 8, padding: 16 },
});
```
### 메소드 목록
| 메소드 | 설명 | TV 동작 |
|---|---|---|
| `play()` | 재생 | 정상 동작 |
| `pause()` | 일시정지 | 정상 동작 |
| `mute()` | 음소거 토글 | 정상 동작 |
| `prev()` / `next()` | 이전/다음 트랙 | 정상 동작 |
| `currentTime(time)` | 시킹 (초) | 정상 동작 |
| `volume(vol)` | 볼륨 설정 | **no-op** (시스템 볼륨 사용) |
| `uiHidden()` / `uiVisible()` | UI 숨김/표시 | 정상 동작 |
| `controlBarActive()` / `controlBarDeactive()` | 컨트롤바 활성화/비활성화 | 정상 동작 |
| `tokenChange(token)` | DRM 토큰 갱신 | 정상 동작 |
| `layout(layout, merge)` | 레이아웃 런타임 변경 | 정상 동작 |
| `changePlayMode(mode)` | VOD/Live 모드 전환 | 정상 동작 |
| `addNextSource(source)` | 다음 소스 추가 | 정상 동작 |
| `addPrevSource(source)` | 이전 소스 추가 | 정상 동작 |
| `isSeeking()` | SeekBar 시킹 상태 조회 | 정상 동작 |
| `fullscreen()` / `fullscreenOn()` / `fullScreenOff()` | 전체화면 | **no-op** (TV 항상 전체화면) |
| `pip()` | PiP | **no-op** (TV 미지원) |
### 동적 플레이리스트 추가
```tsx
// 단일 영상 추가
playerRef.current?.addNextSource({
file: 'https://example.com/new_video.m3u8',
description: { title: '새로운 영상' },
});
// 여러 영상 동시 추가
playerRef.current?.addPrevSource([
{ file: 'https://example.com/video_1.mp4' },
{ file: 'https://example.com/video_2.mp4' },
]);
```
---
## 7. 이벤트
```tsx
{
switch (event.type) {
case 'ready':
console.log('플레이어 준비 완료');
break;
case 'timeupdate':
console.log('현재 시간:', event.state.currentTime);
break;
case 'ended':
console.log('재생 종료');
break;
case 'error':
console.log('에러:', event.data);
break;
}
}}
onBack={() => navigation.goBack()}
/>
```
### 지원 이벤트
| 카테고리 | 이벤트 | 설명 |
|---|---|---|
| 재생 | `ready`, `canplay`, `start` | 준비/시작 |
| | `play`, `playing`, `pause`, `ended` | 재생 상태 |
| | `timeupdate`, `durationchange` | 시간/길이 |
| 버퍼링 | `bufferingStart`, `bufferingEnd` | 버퍼링 |
| | `loadingStart`, `loadingEnd` | 로딩 |
| 시킹 | `seeking`, `seeked`, `waiting` | 시킹 |
| UI | `controlbarActive`, `controlbarDeactive` | 컨트롤바 |
| | `stateChange` | 상태 변경 |
| 트랙 | `next`, `nextTrack`, `prev`, `prevTrack` | 트랙 이동 |
| | `skipForward`, `skipBack` | ±10초 시킹 |
| | `playlistChange`, `quality_change`, `volumechange` | 변경 |
| 광고 | `adStart`, `adStarted`, `adComplete` | 시작/완료 |
| | `adSkip`, `adSkipped`, `adError` | 스킵/에러 |
| | `adLoaded`, `adBreakStart`, `adBreakEnd` | 로드/브레이크 |
| OTT 스킵 | `introSkip`, `openingSkip`, `endingSkip` | 인트로/오프닝/엔딩 스킵 |
| 에러 | `error` | 에러 발생 |
---
## 8. 레이아웃 커스터마이징
5개 섹션(`top`, `upper`, `center`, `lower`, `bottom`) + `seekbar`로 구성된 선언적 레이아웃.
### 사용 가능한 아이템
| 아이템 | 설명 |
|---|---|
| `PlayBtn` | 재생/일시정지/다시재생 |
| `BigPlayBtn` | 중앙 대형 재생 + 이전/다음 |
| `BackBtn` | 뒤로가기 |
| `SeekBar` | 탐색바 (D-pad 좌/우로 시킹) |
| `SkipBackBtn` / `SkipForwardBtn` | ±10초 시킹 |
| `PrevBtn` / `NextBtn` / `NextPrevBtn` | 트랙 이동 |
| `TimeBtn` / `CurrentTimeBtn` / `DurationBtn` | 시간 표시 (라이브 시 LIVE 인디케이터) |
| `MuteBtn` / `VolumeBtn` | 음소거/볼륨 (TV에서는 뮤트 토글) |
| `SubtitleBtn` | 자막 메뉴 (자막 없으면 BlankBtn으로 대체) |
| `SettingBtn` | 설정 메뉴 |
| `MetaDesc` | 영상 메타정보 |
| `Blank` / `BlankBtn` | 빈 공간 |
> **TV에서 제거된 아이템**: `FullscreenBtn`, `PipBtn`, `ShareBtn` — 레이아웃에 포함해도 무시됨
### 커스텀 레이아웃 예제
```tsx
navigation.goBack()}
/>
```
### VOD/Live 변형
```tsx
layout={{
vod: { /* VOD 전용 레이아웃 */ },
live: { /* Live 전용 레이아웃 */ },
}}
```
### 런타임 레이아웃 변경 (ref)
```tsx
// 부분 업데이트 (merge=true, 기본값)
playerRef.current?.layout({ bottom: [{ items: ['PlayBtn'], wrapper: 'Group' }] }, true);
// 전체 교체 (merge=false)
playerRef.current?.layout(newLayout, false);
```
### 레이아웃 타입 정의
```ts
type ControlBarLayoutGroup = {
key?: string;
items: ControlBarLayoutItem[];
noPadding?: boolean;
cap?: number;
wrapper?: "Blank" | "Group";
align?: "left" | "right" | "center";
style?: ViewStyle;
size?: number; // 버튼 크기 (width/height, 기본 88)
};
type ControlBarLayout = {
top?: ControlBarLayoutGroup[];
upper?: ControlBarLayoutGroup[];
center?: ControlBarLayoutGroup[];
lower?: ControlBarLayoutGroup[];
bottom?: ControlBarLayoutGroup[];
order?: Array<"top" | "upper" | "center" | "seekbar" | "lower" | "bottom">;
};
type ControlBarLayoutVariant = {
live?: ControlBarLayout;
vod?: ControlBarLayout;
};
type ControlBarLayoutResponsive = {
pc?: ControlBarLayout | ControlBarLayoutVariant;
mobile?: ControlBarLayout | ControlBarLayoutVariant;
fullscreen?: ControlBarLayout | ControlBarLayoutVariant;
breakpoint?: number;
};
```
---
## 9. IMA 광고
> **TV SDK는 IMA 광고만 지원. NAM 광고(`namAds`)는 미지원.**
```tsx
{
switch (event.type) {
case 'adStart':
console.log('광고 시작');
break;
case 'adComplete':
console.log('광고 완료');
break;
case 'adSkip':
console.log('광고 스킵');
break;
case 'adError':
console.log('광고 에러', event.data);
break;
}
}}
onBack={() => navigation.goBack()}
/>
```
### 네이티브 빌드 설정 (필수)
IMA 광고를 사용하려면 네이티브 빌드 플래그를 활성화해야 한다:
**tvOS** — `ios/Podfile`:
```ruby
$RNVideoUseGoogleIMA = true
```
이후 `pod install` 실행.
**Android TV** — `android/gradle.properties`:
```properties
RNVideo_useExoplayerIMA=true
```
### 광고 동작
- 광고 재생 중 컨트롤바 자동 숨김 + 리모컨 입력 무시
- 네이티브 IMA SDK가 자체 UI 렌더링
- 광고 완료/스킵/에러 후 콘텐츠 자동 재생
---
## 10. 이어보기 (Resume Playback)
SDK는 `onExit`으로 종료 시 재생 정보를 제공하고, `initialPosition`으로 시작 위치를 지정한다.
저장/복원 로직은 앱에서 구현한다.
```tsx
import React, { useRef, useState, useEffect, useCallback } from 'react';
import { View, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { VpePlayer } from '@sgrsoft/vpe-reactnative-tv-sdk';
import type { PlayerHandle, PlayerExitInfo } from '@sgrsoft/vpe-reactnative-tv-sdk';
export default function ResumePlayer() {
const playerRef = useRef(null);
const [initialPosition, setInitialPosition] = useState();
const [ready, setReady] = useState(false);
const sourceUri = 'https://example.com/video.m3u8';
useEffect(() => {
AsyncStorage.getItem(`resume:${sourceUri}`).then(val => {
if (val) {
const saved = JSON.parse(val);
// 완료 근처(5초 이내)면 처음부터
if (saved.time >= saved.duration - 5) {
setInitialPosition(0);
} else {
setInitialPosition(saved.time);
}
}
setReady(true);
});
}, []);
const handleExit = useCallback((info: PlayerExitInfo) => {
AsyncStorage.setItem(`resume:${info.sourceUri}`, JSON.stringify({
time: info.currentTime,
duration: info.duration,
}));
}, []);
if (!ready) return ;
return (
{ /* navigation.goBack() */ }}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#000' },
});
```
### PlayerExitInfo 구조
```ts
{
currentTime: number; // 현재 재생 위치 (초)
duration: number; // 총 길이 (초)
playIndex: number; // 현재 플레이리스트 인덱스
sourceUri: string; // 현재 재생 중인 URL
}
```
---
## 11. 라이브 스트림
라이브 스트림은 자동으로 감지된다 (HLS 매니페스트 분석 + duration 변화 감지).
```tsx
navigation.goBack()}
/>
```
### 라이브 감지 시 자동 동작
- TimeBtn에 빨간 dot + "LIVE" 표시
- 사이드 메뉴에서 재생속도 메뉴 숨김
- SeekBar에서 시킹 비활성화
### lowLatencyMode 동작
- Android TV: ExoPlayer `targetOffsetMs: 2000` (라이브 엣지 2초 목표)
- tvOS: AVPlayer `preferredForwardBufferDuration: 2`
- 5초 이상 뒤처지면 라이브 엣지로 자동 복귀
---
## 12. 에러 처리 (errorOverride)
기본 에러 UI 대신 커스텀 에러 UI를 렌더링할 수 있다.
```tsx
// 방법 1: 렌더 함수 (가장 일반적)
(
에러: {info.errorCode}
{info.errorMessage}
)}
onBack={() => navigation.goBack()}
/>
// 방법 2: React 컴포넌트
// 방법 3: ReactNode
재생 불가} ... />
```
---
## 13. platform 설정 (공공/민간 클라우드)
| 환경 | platform 값 | 설명 |
|---|---|---|
| 민간 클라우드 | `"pub"` | Naver Cloud Platform 민간 환경 (기본값) |
| 공공 클라우드 | `"gov"` | Naver Cloud Platform 공공 환경 |
```tsx
// 민간 클라우드
// 공공 클라우드
```
---
## 14. 옵션 종합 예제
```tsx
navigation.goBack()}
onExit={(info) => {
console.log(`종료: ${info.currentTime}/${info.duration}`);
}}
onEvent={(event) => {
console.log(event.type, event.state);
}}
/>
```
---
## 15. 웹 SDK와의 차이점 요약
| 항목 | 웹 SDK (`vpe-react-sdk`) | TV SDK (`vpe-reactnative-tv-sdk`) |
|---|---|---|
| **플랫폼** | 브라우저 (React/Next.js) | Android TV / tvOS / Fire TV |
| **hls/dashjs** | props로 주입 필수 | **불필요** (네이티브 재생) |
| **전체화면** | `requestFullscreen()` | 항상 전체화면 (no-op) |
| **PiP** | `requestPictureInPicture()` | 미지원 (no-op) |
| **볼륨** | 슬라이더 UI | 시스템 볼륨 (MuteBtn = 뮤트 토글) |
| **입력** | 마우스/키보드 | 리모컨 D-pad |
| **자막** | DOM TextTrack API | VTT/SMI 오버레이 렌더링 |
| **광고** | IMA + NAM | **IMA만** 지원 |
| **아이콘** | `icon` 옵션으로 커스터마이징 | 기본 아이콘 고정 |
| **TV 전용 props** | - | `onBack`, `onExit`, `initialPosition` |
| **제거된 아이템** | - | FullscreenBtn, PipBtn, ShareBtn |
| **제거된 옵션** | - | aspectRatio, objectFit, namAds |
| **setup() / ncplayer** | 지원 | **미지원** (VpePlayer 컴포넌트만) |
---
## 16. 플랫폼별 참고사항
### 해상도
| 플랫폼 | 보고 해상도 | 스케일 | 실제 해상도 |
|---|---|---|---|
| Apple TV | 1920 x 1080 | 1x | 1920 x 1080 |
| Android TV | 960 x 540 | 2x | 1920 x 1080 |
> Android TV는 960x540 dp로 보고. SDK가 내부적으로 자동 스케일 보정.
### 리모컨 이벤트
| 이벤트 | Apple TV | Android TV | 동작 |
|---|---|---|---|
| select | O | O | 확인/선택 |
| playPause | O | O | 재생/일시정지 |
| up/down | O | O | 컨트롤바 표시/포커스 이동 |
| left/right | O | O | SeekBar에서 ±10초 시킹 |
| menu | O | X | 뒤로가기 |
| play/pause | X | O | 재생/일시정지 전용 |
| rewind/fastForward | X | O | 되감기/빨리감기 |
### 지원 버전
| 항목 | 최소 | 권장 |
|---|---|---|
| react-native-tvos | 0.83.1-1 | 0.84.0-0 |
| React | 19.x | 19.2.x |
| Node.js | 22.11+ | 22.x LTS |
| tvOS | 15.1+ | 16.0+ |
| Android API | 24 (7.0) | 28+ |
| TypeScript | 5.0+ | 5.8.x |
---
## 17. FAQ
**Q. 재생이 안 됩니다.**
- `accessKey`가 유효한지 확인
- `playlist`에 `file`이 포함되어 있는지 확인
- 스트림 URL이 접근 가능한지 확인
**Q. DRM 재생이 실패합니다.**
- DRM은 시뮬레이터/에뮬레이터에서 동작하지 않음 — 실기기 테스트 필수
- `licenseUri`, `licenseRequestHeader` 값 확인
- Android TV → Widevine(`com.widevine.alpha`), tvOS → FairPlay(`com.apple.fps`)
**Q. IMA 광고가 나오지 않습니다.**
- 네이티브 빌드 플래그 확인: tvOS `$RNVideoUseGoogleIMA = true`, Android `RNVideo_useExoplayerIMA=true`
- 플래그 변경 후 **앱 리빌드** 필수 (Metro 핫 리로드 아님)
**Q. 자막이 안 보입니다.**
- `playlist[].vtt` 배열에 자막 트랙이 포함되어 있는지 확인
- VTT/SMI 파일 URL이 접근 가능한지 확인
- SMI 파일의 EUC-KR 인코딩은 자동 디코딩됨
**Q. Android TV에서 D-pad가 동작하지 않습니다.**
- 에뮬레이터 `config.ini`에서 `hw.keyboard = yes` 확인
- 경로: `~/.android/avd/.avd/config.ini` → 변경 후 에뮬레이터 재시작
**Q. 컨트롤바가 보이지 않습니다.**
- `options.controls`가 `true`인지 확인
- `uiHidden()`이 호출된 상태라면 `uiVisible()`로 복원
- 리모컨 아무 버튼 누르면 컨트롤바 활성화
**Q. `src` 수정 후 반영이 안 됩니다.**
- `vpe-react-native-ui`의 `src/` 수정 후 반드시 `yarn prepare` (bob build) 실행
- Metro가 빌드된 `lib/module/`을 사용하므로 `src/` 직접 수정은 즉시 반영 안 됨
**Q. 웹 SDK의 `setup()` / `ncplayer`를 사용할 수 있나요?**
- TV SDK에서는 `` 컴포넌트만 지원. `setup()`, `ncplayer` 클래스는 미지원.