@sgrsoft/vpe-reactnative-tv-sdk
v0.1.2
Published
VPE React Native TV SDK for Android TV / Apple TV (tvOS)
Readme
@sgrsoft/vpe-reactnative-tv-sdk
Android TV / Apple TV(tvOS) / Fire TV 비디오 플레이어 SDK
React Native(react-native-tvos) 기반의 TV 전용 비디오 플레이어 SDK.
웹 SDK(vpe-react-sdk)와 동일한 API를 제공하여 플랫폼 간 일관된 개발 경험을 지원한다.
주요 기능
- HLS / DASH / MP4 네이티브 재생 (ExoPlayer / AVPlayer)
- DRM 지원 (Widevine + FairPlay)
- IMA 광고 (Google Interactive Media Ads)
- 플레이리스트 (자동 다음 재생)
- 자막 (VTT / SMI, EUC-KR 자동 디코딩)
- 선언적 레이아웃 커스터마이징
- 라이브 스트림 + LL-HLS 저지연 모드
- 다국어 (ko / en / ja)
- 이어보기 (onExit + initialPosition)
- D-pad / 리모컨 네비게이션
- HLS ABR 화질 선택
- 캡처 방지
- vpe-core-sdk 연동 (라이선스, MA 분석)
설치
# npm
npm install @sgrsoft/vpe-reactnative-tv-sdk
# yarn
yarn add @sgrsoft/vpe-reactnative-tv-sdkpeerDependencies 설치
yarn add react react-native @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:[email protected]"
선택적 의존성
# 캡처 방지 기능 사용 시
yarn add react-native-capture-protectioniOS (tvOS)
cd ios && pod installAndroid TV
IMA 광고를 사용하는 경우 android/gradle.properties에 추가:
RNVideo_useExoplayerIMA=true빠른 시작
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';
function App() {
const playerRef = useRef<PlayerHandle>(null);
return (
<View style={styles.container}>
<VpePlayer
ref={playerRef}
accessKey="YOUR_ACCESS_KEY"
options={{
playlist: [{
file: 'https://example.com/video.m3u8',
description: { title: '영상 제목' },
}],
autostart: true,
}}
onBack={() => {
// 뒤로가기 처리
}}
onEvent={(event) => {
if (event.type === 'ready') {
console.log('Player ready');
}
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#000' },
});Props (PlayerProps)
| Prop | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| accessKey | string | - | API 접근 키 (필수) |
| devAppId | string | - | 로컬 테스트용 앱 식별자. 자동 감지 실패 시 fallback |
| platform | 'pub' \| 'gov' | 'pub' | 플랫폼 |
| stage | string | 'prod' | 스테이지 |
| isDev | boolean | false | 개발 모드 |
| options | PlayerOptions | - | 플레이어 옵션 |
| layout | ControlBarLayout \| ControlBarLayoutVariant \| ControlBarLayoutResponsive | - | 레이아웃 커스터마이징 |
| scopeId | string | - | 스코프 ID |
| onEvent | (event: PlayerEvent) => void | - | 이벤트 핸들러 |
| onBack | () => void | - | 뒤로가기 콜백 (TV 전용) |
| onExit | (info: PlayerExitInfo) => void | - | 플레이어 종료 시 재생 정보 전달 (TV 전용) |
| initialPosition | number | - | 시작 위치 (초 단위, 이어보기용, TV 전용) |
| errorOverride | ReactNode \| ComponentType \| Function | - | 커스텀 에러 UI |
Options (PlayerOptions)
재생
| 옵션 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| playlist | PlaylistItem[] | - | 재생할 미디어 목록 |
| autostart | boolean | true | 자동 재생 |
| muted | boolean | false | 초기 음소거 |
| repeat | boolean | false | 반복 재생 |
| playIndex | number | 0 | 시작 재생 인덱스 |
| token | string | - | 스트림 URL 토큰 |
| lowLatencyMode | boolean | false | LL-HLS 저지연 모드 |
UI
| 옵션 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| controls | boolean | true | 컨트롤바 표시 |
| controlActiveTime | number | 3000 | 컨트롤바 자동 숨김 (ms) |
| progressBarColor | string | - | 진행바 색상 |
| controlBtn | object | - | 버튼별 표시 on/off |
| descriptionNotVisible | boolean | false | 메타 설명 숨김 |
| lang | string | 'ko' | UI 언어 (ko / en / ja) |
| playRateSetting | number[] | [0.5, 0.75, 1, 1.5, 2] | 재생 속도 목록 |
자막 / 워터마크
| 옵션 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| visibleWatermark | boolean | false | 워터마크 표시 |
| watermarkText | string | - | 워터마크 텍스트 |
| watermarkConfig | object | - | 워터마크 위치/투명도/랜덤 이동 설정 |
광고 / 보안
| 옵션 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| ads | { tagUrl: string; enabled?: boolean } | - | IMA 광고 설정 |
| screenRecordingPrevention | boolean | false | 캡처/녹화 방지 (TV 전용) |
PlaylistItem
type PlaylistItem = {
file?: string; // 미디어 URL (.mp4, .m3u8, .mpd)
poster?: string; // 포스터 이미지 URL
drm?: DrmConfig; // DRM 설정
description?: {
title?: string;
profile_name?: string;
profile_image?: string;
created_at?: string;
};
vtt?: Array<{ // 자막 트랙
label?: string; // 표시 이름 ("한국어", "English")
src?: string; // VTT 또는 SMI 파일 URL
default?: boolean; // 기본 자막 여부
}>;
};자막 예제
options={{
playlist: [{
file: 'https://example.com/video.m3u8',
vtt: [
{ label: '한국어', src: 'https://example.com/ko.vtt', default: true },
{ label: 'English', src: 'https://example.com/en.vtt' },
{ label: '日本語', src: 'https://example.com/ja.smi' }, // SMI도 지원
],
}],
}}ref 메소드 (PlayerHandle)
const playerRef = useRef<PlayerHandle>(null);
// 재생 제어
playerRef.current?.play();
playerRef.current?.pause();
playerRef.current?.mute();
// 시킹
playerRef.current?.currentTime(30); // 30초로 이동
// 플레이리스트
playerRef.current?.next();
playerRef.current?.prev();
playerRef.current?.addNextSource({ file: 'https://...' });
playerRef.current?.addPrevSource({ file: 'https://...' });
// 토큰 갱신
playerRef.current?.tokenChange('NEW_TOKEN');
// 레이아웃 변경
playerRef.current?.layout({ bottom: [{ items: ['PlayBtn'], wrapper: 'Group' }] }, true);
// 컨트롤바 제어
playerRef.current?.controlBarActive();
playerRef.current?.controlBarDeactive();
playerRef.current?.uiHidden();
playerRef.current?.uiVisible();
// 재생 모드 강제 전환
playerRef.current?.changePlayMode('live');| 메소드 | 설명 | TV 참고사항 |
|--------|------|------------|
| play() | 재생 | - |
| pause() | 일시정지 | - |
| mute() | 음소거 토글 | - |
| prev() / next() | 트랙 이동 | - |
| currentTime(time) | 시킹 (초) | - |
| volume(vol) | 볼륨 설정 | TV: no-op (시스템 볼륨 사용) |
| layout(layout, merge) | 레이아웃 변경 | - |
| tokenChange(token) | DRM 토큰 갱신 | - |
| addNextSource(source) | 다음 소스 추가 | - |
| addPrevSource(source) | 이전 소스 추가 | - |
| uiHidden() / uiVisible() | 컨트롤바 숨김/표시 | - |
| controlBarActive() / controlBarDeactive() | 컨트롤바 활성화/비활성화 | - |
| changePlayMode(mode) | VOD/Live 모드 전환 | - |
| isSeeking() | SeekBar 시킹 상태 조회 | - |
| fullscreen() / pip() | 전체화면 / PiP | TV: no-op (항상 전체화면, PiP 미지원) |
이벤트 (PlayerEvent)
<VpePlayer
onEvent={(event) => {
console.log(event.type, event.state);
}}
/>이벤트 타입
| 카테고리 | 이벤트 | 설명 |
|---------|--------|------|
| 재생 | 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 | 광고 로드/브레이크 |
| 에러 | error | 에러 발생 |
DRM 설정
Widevine (Android TV)
options={{
playlist: [{
file: 'https://example.com/video.mpd',
drm: {
'com.widevine.alpha': {
licenseUri: 'https://license-server.com/widevine',
licenseRequestHeader: {
'pallycon-customdata-v2': 'TOKEN',
},
},
},
}],
}}FairPlay (tvOS)
options={{
playlist: [{
file: 'https://example.com/video.m3u8',
drm: {
'com.apple.fps': {
licenseUri: 'https://license-server.com/fairplay',
certificateUri: 'https://license-server.com/cert',
certificateRequestHeader: {
'pallycon-customdata-v2': 'TOKEN',
},
},
},
}],
}}react-native-video 직접 DRM 형식
options={{
playlist: [{
file: 'https://example.com/video.m3u8',
drm: {
type: 'fairplay',
licenseServer: 'https://license-server.com/fairplay',
certificateUrl: 'https://license-server.com/cert',
headers: { 'pallycon-customdata-v2': 'TOKEN' },
},
}],
}}IMA 광고
<VpePlayer
accessKey="YOUR_ACCESS_KEY"
options={{
playlist: [{ file: 'https://example.com/video.m3u8' }],
ads: {
tagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?...',
enabled: true, // 기본: true
},
}}
/>네이티브 빌드 설정 (필수)
tvOS — ios/Podfile:
$RNVideoUseGoogleIMA = true이후 pod install 실행.
Android TV — android/gradle.properties:
RNVideo_useExoplayerIMA=true광고 재생 중에는 컨트롤바가 자동으로 숨겨지며, 네이티브 IMA SDK가 자체 UI를 렌더링한다.
레이아웃 커스터마이징
5개 섹션(top, upper, center, lower, bottom) + seekbar으로 구성된 선언적 레이아웃 시스템을 제공한다.
사용 가능한 아이템
| 아이템 | 설명 |
|--------|------|
| PlayBtn | 재생/일시정지/다시재생 |
| BigPlayBtn | 중앙 대형 재생 + 이전/다음 |
| BackBtn | 뒤로가기 |
| SeekBar | 탐색바 |
| SkipBackBtn / SkipForwardBtn | ±10초 시킹 |
| PrevBtn / NextBtn / NextPrevBtn | 트랙 이동 |
| TimeBtn / CurrentTimeBtn / DurationBtn | 시간 표시 |
| MuteBtn / VolumeBtn | 음소거/볼륨 |
| SubtitleBtn | 자막 메뉴 |
| SettingBtn | 설정 메뉴 |
| MetaDesc | 영상 메타정보 |
| Blank / BlankBtn | 빈 공간 |
커스텀 레이아웃 예제
<VpePlayer
accessKey="YOUR_ACCESS_KEY"
options={{ playlist: [...] }}
layout={{
top: [
{ items: ['BackBtn'], wrapper: 'Group' },
{ items: ['MetaDesc'] },
{ wrapper: 'Blank', items: [] },
],
center: [
{ items: ['BigPlayBtn'], align: 'center' },
],
lower: [
{ wrapper: 'Blank', items: ['SeekBar'], align: 'center' },
],
bottom: [
{ items: ['PlayBtn'], wrapper: 'Group' },
{ items: ['NextPrevBtn'], wrapper: 'Group' },
{ items: ['MuteBtn'], wrapper: 'Group' },
{ items: ['TimeBtn'], wrapper: 'Group' },
{ wrapper: 'Blank', items: [] },
{ items: ['SubtitleBtn', 'SettingBtn'], wrapper: 'Group' },
],
}}
/>VOD / Live 변형
layout={{
vod: { /* VOD 전용 레이아웃 */ },
live: { /* Live 전용 레이아웃 */ },
}}런타임 레이아웃 변경
// 부분 업데이트 (merge=true, 기본값)
playerRef.current?.layout({ bottom: [{ items: ['PlayBtn'], wrapper: 'Group' }] }, true);
// 전체 교체 (merge=false)
playerRef.current?.layout(newLayout, false);라이브 스트림
라이브 스트림은 자동으로 감지된다:
- HLS 매니페스트에서
#EXT-X-ENDLIST부재 확인 - Duration이 0 또는 Infinity
- 재생 중 duration 변화 감지
라이브 감지 시:
- TimeBtn에 빨간 dot + "LIVE" 표시
- 사이드 메뉴에서 재생속도 메뉴 숨김
- SeekBar에서 시킹 비활성화
LL-HLS 저지연 모드
options={{
lowLatencyMode: true,
playlist: [{ file: 'https://example.com/live.m3u8' }],
}}- Android TV: ExoPlayer
targetOffsetMs: 2000 - tvOS: AVPlayer
preferredForwardBufferDuration: 2 - 5초 이상 뒤처지면 라이브 엣지로 자동 복귀
이어보기 (Resume Playback)
SDK는 onExit 콜백으로 종료 시 재생 정보를 제공하고, initialPosition으로 시작 위치를 지정한다. 저장/복원 로직은 앱에서 구현한다.
function PlayerScreen() {
const [startPos, setStartPos] = useState<number | undefined>();
useEffect(() => {
// 저장된 위치 불러오기
AsyncStorage.getItem('lastPosition').then(val => {
if (val) setStartPos(Number(val));
});
}, []);
return (
<VpePlayer
accessKey="YOUR_ACCESS_KEY"
options={{ playlist: [...] }}
initialPosition={startPos}
onExit={(info) => {
// info: { currentTime, duration, playIndex, sourceUri }
AsyncStorage.setItem('lastPosition', String(info.currentTime));
}}
/>
);
}에러 처리 (errorOverride)
기본 에러 UI 대신 커스텀 에러 UI를 렌더링할 수 있다.
// 함수 패턴
<VpePlayer
errorOverride={(info) => (
<View>
<Text>에러: {info.errorCode}</Text>
<Text>{info.errorMessage}</Text>
</View>
)}
/>
// 컴포넌트 패턴
<VpePlayer errorOverride={MyErrorComponent} />
// ReactNode 패턴
<VpePlayer errorOverride={<Text>에러가 발생했습니다</Text>} />플랫폼별 참고사항
해상도
| 플랫폼 | 보고 해상도 | 스케일 | 실제 해상도 | |--------|-----------|--------|-----------| | 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/left/right | O | O | 방향키 |
| menu | O | X | 메뉴 (뒤로가기) |
| play / pause | X | O | 재생/일시정지 전용 |
| rewind / fastForward | X | O | 되감기/빨리감기 |
웹 SDK와의 차이점
| 기능 | 웹 | TV |
|------|-----|-----|
| 전체화면 | requestFullscreen() | 항상 전체화면 (no-op) |
| PiP | requestPictureInPicture() | 미지원 (no-op) |
| 볼륨 | 슬라이더 UI | 시스템 볼륨 (MuteBtn = 뮤트 토글) |
| 입력 | 마우스/키보드 | 리모컨 D-pad |
| 자막 | DOM TextTrack API | VTT/SMI 오버레이 렌더링 |
| 광고 | IMA + NAM | IMA만 지원 |
버전 호환성
| 항목 | 최소 | 권장 | |------|------|------| | 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 |
라이선스
MIT
