# 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` 클래스는 미지원.