# VpePlayer — AI-Optimized Reference > 이 문서는 LLM/AI 코드 생성기 전용. 인간이 읽기 위한 prose 는 의도적으로 제거됨. > 4개 카테고리로만 구성: **Rules / Schemas / Canonical Templates / Forbidden Patterns / Concise Rulesets**. --- ## Rules (코드 생성 전 STRICT) 1. 재생소스는 `options.playlist` 배열로만 전달. `src` / `source` / `url` / `videoUrl` 같은 prop 없음. 2. hls.js → top-level `import Hls from "hls.js"` (SSR 안전). 3. dashjs → 반드시 `useEffect` 내 dynamic import. top-level import 시 Next.js SSR `navigator is not defined`. 4. Next.js App Router 클라이언트 컴포넌트 → 파일 최상단 `"use client"` 필수. 5. 환경 우선순위: Next.js > React (Vite/CRA) > React Native > UMD. 6. UMD (` import { VpePlayer } from "@sgrsoft/vpe-react-sdk"; ``` ```tsx // ❌ WRONG — Sub Account 가 아닌 Master API key 를 프론트엔드에 노출 accessKey="MASTER_API_KEY_xxx" // ✅ CORRECT — Sub Account 의 access key 만 사용 (Master key 는 backend 전용) accessKey="SUB_ACCOUNT_ACCESS_KEY" ``` ```tsx // ❌ WRONG — autostart: true + muted: false 를 모든 환경에서 동작한다고 가정 options={{ autostart: true, muted: false, // ★ 새로고침/북마크/직접 진입 시 브라우저 정책으로 거부됨 ★ playlist: [{ file: "..." }], }} // → user gesture 없는 진입(새로고침/북마크/URL 입력)에서 자동재생 실패. // → SDK 가 muted 로 자동 fallback 하지 않음. 영상이 멈춰 있는 상태로 남음. // ✅ CORRECT (안전) — 항상 muted: true 로 시작, 사용자 인터랙션 후 unmute options={{ autostart: true, muted: true, // 모든 환경에서 자동재생 허용 playlist: [{ file: "..." }], }} // ✅ CORRECT (조건부 unmuted) — 실패 시 muted fallback + 사용자 안내 const ref = useRef(null); { if (e.type === "error") { ref.current?.setMuted(true); ref.current?.play(); // 사용자에게 "🔊 소리 켜기" 버튼 노출 } }} /> ``` ```tsx // ❌ WRONG — "autostart: true 면 무조건 재생됨" 가정한 코드 useEffect(() => { // 자동재생이 항상 성공할 거라 가정하고 의존성 작업 진행 sendAnalyticsEvent("video_playing"); }, []); // ✅ CORRECT — "play" 이벤트로 실제 재생 시작 시점 확인 { if (e.type === "play") sendAnalyticsEvent("video_playing"); }} /> ``` ```tsx // ❌ WRONG — iOS Safari 에서 muted 없이 autostart 가능하다고 가정 // iOS 는 muted + playsInline 만 자동재생 허용. unmuted 는 항상 user gesture 필수. options={{ autostart: true, muted: false, playlist: [...] }} // iOS 에서 항상 실패 // ✅ CORRECT — iOS 포함 모든 환경 지원 options={{ autostart: true, muted: true, playlist: [...] }} // playsInline 은 SDK 가 video 엘리먼트에 자동 적용 ``` ```tsx // ❌ WRONG — navigator.share 가 모든 환경에서 동작한다고 가정 const share = () => { navigator.share({ title, url }); // ★ Desktop Safari, http, Firefox Desktop 등에서 throw ★ }; // ✅ CORRECT — feature detection + fallback 필수 const share = async () => { if (navigator.share) { try { await navigator.share({ title, url }); return; } catch (e) { /* 사용자 취소 또는 환경 미지원 */ } } // Fallback: URL 클립보드 복사 await navigator.clipboard.writeText(url); alert("URL 이 복사되었습니다."); }; ``` ```tsx // ❌ WRONG — iPhone Safari 에서 표준 Fullscreen API 호출 시도 playerRef.current.querySelector(".vpe-root-wrap").requestFullscreen(); // iPhone 에서 throw document.exitFullscreen(); // iPhone 에서 동작 안 함 // ✅ CORRECT — SDK 의 fullscreen 메소드 사용 (플랫폼별 자동 분기) const playerRef = useRef(null); playerRef.current?.fullscreen(); // SDK 가 플랫폼에 맞게 처리 // iPhone: video.webkitEnterFullscreen() — 네이티브 풀스크린 // Desktop/Android: element.requestFullscreen() — 표준 API // iPhone 풀스크린 시 SDK 커스텀 컨트롤이 가려진다는 사실 사용자에게 안내 ``` ```tsx // ❌ WRONG — iPhone 풀스크린에서도 커스텀 컨트롤바가 보일 거라고 가정 options={{ autostart: true, muted: true, playlist: [...], // iosFullscreenNativeMode 기본값(true) → iPhone 풀스크린 시 iOS 네이티브 컨트롤 강제 노출 // 커스텀 컨트롤바는 풀스크린에서 가려짐 }} // ✅ CORRECT — iPhone 에서도 커스텀 컨트롤바 유지하려면 false 명시 + OS UI 한계 안내 options={{ autostart: true, muted: true, iosFullscreenNativeMode: false, // CSS 기반 가짜 풀스크린 — SDK 커스텀 컨트롤 유지 // ※ 단, iOS 상태바/홈 인디케이터 등 OS UI 는 가릴 수 없음 playlist: [...], }} ``` ```tsx // ❌ WRONG — layout 에 존재하지 않는 preset / variant / boolean 필드 사용 layout={{ variant: "live-commerce", // ★ variant 같은 preset 필드 없음 ★ variant: "minimal", variant: "ott", preset: "youtube-style", template: "default", theme: "dark", responsive: true, // ★ responsive: boolean 같은 필드 없음 ★ autoLayout: true, }} // ❌ WRONG — 영역명을 임의로 (top/upper/center/lower/bottom 만 존재) layout={{ header: [...], // X footer: [...], // X sidebar: [...], // X main: [...], // X controls: [...], // X }} // ❌ WRONG — items 가 객체/문자열 직접 (배열로 감싸야 함) layout={{ lower: "PlayBtn" }} layout={{ lower: { items: "PlayBtn" } }} // ✅ CORRECT — 명시적 영역(top/upper/center/lower/bottom) + 그룹 배열 + items 배열 layout={{ upper: [{ items: ["SeekBar"] }], lower: [ { align: "left", items: ["PlayBtn", "VolumeBtn", "CurrentTimeBtn", "DurationBtn"] }, { align: "right", items: ["SubtitleBtn", "SettingBtn", "FullscreenBtn"] }, ], }} // ✅ CORRECT — 반응형 분기 (responsive: true 같은 boolean 아님, 객체로 분기) layout={{ pc: { upper: [{ items: ["SeekBar"] }], lower: [...] }, mobile: { upper: [{ items: ["SeekBar"] }], lower: [...] }, breakpoint: 768, }} // ✅ CORRECT — 라이브/VOD 분기 layout={{ pc: { live: { lower: [{ items: ["PlayBtn", "MuteBtn", "FullscreenBtn"] }] }, vod: { lower: [{ items: ["PlayBtn", "CurrentTimeBtn", "DurationBtn"] }] }, }, }} // ⚠️ 세부 디자인은 VPE Player Editor (https://vpe-player-editor.web.app/) 에서 // 시각적으로 편집 후 export 된 객체를 그대로 붙여넣을 것을 권장. // "live-commerce", "minimal" 같은 preset 이름을 LLM 이 추측해서 생성하지 마라. ``` ```tsx // ❌ WRONG — layout 안에 잘못된 그룹 속성 layout={{ lower: [{ align: "middle", // ★ "middle" 없음. left | right | center 만 ★ align: "top", align: "bottom", wrapper: "Card", // ★ Group | Blank 만 ★ wrapper: "Pill", }], }} // ✅ CORRECT layout={{ lower: [{ align: "left", // "left" | "right" | "center" wrapper: "Group", // "Group" (둥근 배경) | "Blank" (배경 없음) items: [...], }], }} ``` ```tsx // ❌ WRONG — 존재하지 않는 ControlBarLayoutItem 이름 사용 layout={{ lower: [{ items: [ "PlayButton", // X — "PlayBtn" "Volume", // X — "VolumeBtn" "Time", // X — "TimeBtn" / "CurrentTimeBtn" / "DurationBtn" "Captions", // X — "SubtitleBtn" "Settings", // X — "SettingBtn" "Fullscreen", // X — "FullscreenBtn" "PictureInPicture", // X — "PipBtn" "Share", // X — "ShareBtn" "BigPlay", // X — "BigPlayBtn" "Progress", // X — "SeekBar" ]}], }} // ✅ CORRECT — 정확한 21개 아이템 이름만 (Schemas/ControlBarLayout 참조) layout={{ lower: [{ items: [ "PlayBtn", "VolumeBtn", "MuteBtn", "TimeBtn", "CurrentTimeBtn", "DurationBtn", "SubtitleBtn", "SettingBtn", "FullscreenBtn", "PipBtn", "PrevBtn", "NextBtn", "NextPrevBtn", "MetaDesc", "BigPlayBtn", "SeekBar", "SettingModal", "SkipForwardBtn", "SkipBackBtn", "ShareBtn", "Blank", ]}], }} ``` ```tsx // ❌ WRONG — DOM 직접 조작 document.querySelector("video")?.play(); document.querySelector(".vpe-control-bar").style.display = "none"; // ✅ CORRECT — Ref 메소드 / options 사용 const ref = useRef(null); ref.current?.play(); // 컨트롤바 숨기려면: options={{ controls: false }} ``` ```tsx // ❌ WRONG — captionType 이 "native" 인데 captionStyle 도 같이 지정 (무시됨, 혼란 유발) options={{ captionType: "native", captionStyle: { fontSize: 20, color: "#fff" }, // ← 아무 효과 없음 }} // ✅ CORRECT — html 모드에서 captionStyle 사용 options={{ captionType: "html", captionStyle: { fontSize: 20, color: "#fff" }, }} ``` ```tsx // ❌ WRONG — vtt 항목에 src 와 file 동시 지정 (혼란) vtt: [{ id: "ko", src: "ko.vtt", file: "ko.vtt", label: "한국어" }] // ✅ CORRECT — src 만 사용 (V2 표준) vtt: [{ id: "ko", src: "ko.vtt", label: "한국어" }] // 또는 V1 호환: file 만 사용 (자동으로 src 로 정규화됨) vtt: [{ id: "ko", file: "ko.vtt", label: "한국어" }] ``` ```tsx // ❌ WRONG — options 최상위에 자막 등록 (이런 옵션 없음) options={{ subtitles: [{ ... }], captions: [{ ... }], vtt: [{ ... }], tracks: [{ ... }], }} // ✅ CORRECT — playlist 항목 안의 vtt 배열에만 등록 options={{ playlist: [{ file: "https://.../master.m3u8", vtt: [{ id: "ko", src: "...", label: "한국어", default: true }], }], }} ``` ```tsx // ❌ WRONG — HTML 요소를 VpePlayer children 으로 (동작 안 함) // ✅ CORRECT — playlist[].vtt[] 로 ``` ```tsx // ❌ WRONG — captionType / captionStyle 에 자막 URL 등록 시도 options={{ captionType: "https://.../ko.vtt", // 잘못된 사용 captionStyle: { src: "https://.../ko.vtt", ... }, // 잘못된 사용 }} // ✅ CORRECT — 역할 분리: // - playlist[].vtt[] → 자막 소스 등록 (필수) // - captionType → 렌더링 방식 선택 ("html" | "native") // - captionStyle → 자막 텍스트 스타일링 options={{ playlist: [{ file: "...", vtt: [{ id: "ko", src: "ko.vtt", label: "한국어" }] }], captionType: "html", captionStyle: { fontSize: 20, color: "#fff" }, }} ``` ```tsx // ❌ WRONG — captionType 값을 임의로 (오타/추측) captionType: "div" captionType: "overlay" captionType: "custom" captionType: "webvtt" // ✅ CORRECT — 정확히 "html" 또는 "native" 만 captionType: "html" // 기본 — div 오버레이로 렌더, captionStyle 적용 captionType: "native" // 브라우저 ::cue, OS 접근성 자막 설정 따름 ``` ```tsx // ❌ WRONG — edgeStyle 값을 임의로 (오타/추측) edgeStyle: "shadow" edgeStyle: "outline" edgeStyle: "stroke" edgeStyle: "border" // ✅ CORRECT — 정확히 5개 값 중 하나 edgeStyle: "none" // 효과 없음 edgeStyle: "dropshadow" // 그림자 edgeStyle: "raised" // 양각 edgeStyle: "depressed" // 음각 edgeStyle: "uniform" // 4방향 외곽선 (기본, 가독성 권장) ``` ```tsx // ❌ WRONG — track.mode 또는 videoEl 의 textTracks 를 직접 조작 videoRef.current.textTracks[0].mode = "showing"; document.querySelector("video").textTracks[0].mode = "hidden"; // ✅ CORRECT — Ref 메소드 사용 (SDK 가 mode 자동 관리) playerRef.current?.setSubtitleEnabled(true); // 자막 ON playerRef.current?.setSubtitleEnabled(false); // 자막 OFF playerRef.current?.toggleSubtitle(); // 토글 playerRef.current?.selectSubTitle(idx); // 특정 인덱스 자막 선택 ``` ```tsx // ❌ WRONG — default 를 여러 vtt 항목에 동시 true (예측 불가) vtt: [ { id: "ko", src: "ko.vtt", label: "한국어", default: true }, { id: "en", src: "en.vtt", label: "English", default: true }, ] // ✅ CORRECT — 한 항목만 default: true vtt: [ { id: "ko", src: "ko.vtt", label: "한국어", default: true }, { id: "en", src: "en.vtt", label: "English" }, ] ``` ```tsx // ❌ WRONG — captionStyle 의 반응형 값을 잘못된 형식으로 captionStyle: { fontSize: [20, 14], // 배열 X fontSize: "20px pc, 14px mobile", // 문자열 X fontSize: { desktop: 20, phone: 14 }, // desktop/phone 키 X } // ✅ CORRECT — 단일값 또는 { pc, mobile } 객체 captionStyle: { fontSize: 20, // PC/모바일 동일 fontSize: "1.2rem", // CSS 단위 문자열 fontSize: { pc: 20, mobile: 14 }, // 반응형 분기 } ``` ```tsx // ❌ WRONG — captionStyle 에 임의의 CSS 속성 추가 (지원 안 함) captionStyle: { border: "2px solid red", // 미지원 boxShadow: "0 0 10px black", // 미지원 margin: "10px", // 미지원 transform: "scale(1.2)", // 미지원 fontWeight: "bold", // 미지원 } // ✅ CORRECT — 지원되는 필드만 사용 (Schemas/captionStyle 참조) captionStyle: { fontSize, fontFamily, color, backgroundColor, opacity, lineHeight, edgeStyle, bottomOffset, sidePadding, padding, } ``` ```tsx // ❌ WRONG — Ref 에 존재하지 않는 자막 관련 메소드 호출 playerRef.current?.loadSubtitle("ko.vtt"); playerRef.current?.addSubtitle({ src: "ko.vtt" }); playerRef.current?.setSubtitleSrc("ko.vtt"); playerRef.current?.changeCaption(0); // ✅ CORRECT — 실제 존재하는 메소드만 playerRef.current?.toggleSubtitle(); playerRef.current?.setSubtitleEnabled(true); playerRef.current?.selectSubTitle(idx); // 자막 소스 자체를 바꾸려면 options.playlist 를 새 vtt 로 교체 (props 변경 → 재마운트) ``` ```tsx // ❌ WRONG — playlist 가 단일 객체 options={{ playlist: { file: "..." } }} // ✅ CORRECT — 반드시 배열 options={{ playlist: [{ file: "..." }] }} ``` ```tsx // ❌ WRONG — playlist[].file 누락 (drm.src 만 있어도 안 됨) playlist: [{ drm: { "com.widevine.alpha": { src: "..." } } }] // ✅ CORRECT — file 필수 playlist: [{ file: "https://.../master.m3u8", drm: { "com.widevine.alpha": {...} } }] ``` ```tsx // ❌ WRONG — accessKey 누락 또는 빈 문자열 // ✅ CORRECT ``` ```tsx // ❌ WRONG — onEvent 콜백을 매 render 시 inline 생성 (불필요한 리렌더) { /* 핸들러 */ }} ... /> // ✅ CORRECT — useCallback 으로 안정된 참조 const handleEvent = useCallback((e: PlayerEvent) => { /* 핸들러 */ }, []); ``` ```tsx // ❌ WRONG — Next.js Pages Router 에서 SSR 페이지에 직접 VpePlayer // pages/index.tsx — getServerSideProps 안에서 import 등 import { VpePlayer } from "@sgrsoft/vpe-react-sdk"; // 빌드 OK 지만 SSR 에러 // ✅ CORRECT — Next.js dynamic import 로 ssr: false import dynamic from "next/dynamic"; const VpePlayer = dynamic( () => import("@sgrsoft/vpe-react-sdk").then((m) => m.VpePlayer), { ssr: false } ); ``` ```tsx // ❌ WRONG — platform 을 임의의 문자열로 platform="custom" // ✅ CORRECT — "pub" (민간) 또는 "gov" (공공) 만 platform="pub" // 또는 "gov" ``` ```tsx // ❌ WRONG — UI 텍스트를 직접 컴포넌트로 override (텍스트는 lang 옵션으로) 재생 // ✅ CORRECT — lang 옵션 또는 icon override options={{ lang: "ko" }} // 언어 설정 options={{ icon: { play: } }} // 아이콘 교체 ``` ```tsx // ❌ WRONG — DRM token 을 클라이언트에서 직접 생성/계산 const token = btoa(JSON.stringify({ userId, expiry: Date.now() + 3600000 })); // ✅ CORRECT — backend 에서 발급받은 token 만 사용 const { token } = await fetch("/api/drm-token").then(r => r.json()); ``` --- ## Rulesets ### 기본 동작 - `playlist` 는 항상 배열. 단일 객체 X - `playlist[].file` 은 항상 필수 (drm.src 만 있어도 안 됨) - `autostart: true` 사용 시 `muted: true` 도 함께 (브라우저 autoplay 정책 — 자세한 건 아래 "자동재생" 참조) - `controls: false` 로 컨트롤바 전체 숨김 가능 (커스텀 UI 직접 구현 시) - `aspectRatio` 기본 "16/9", 세로 영상은 "9/16" 등 명시 - `repeat: true` 로 반복 재생, 플레이리스트와 함께 사용 시 마지막 항목 재시작 ### 자동재생 (Autoplay) **핵심 한계 (반드시 사용자에게 안내)**: - **음소거 없이 자동재생은 user gesture 가 있어야 가능**. - 다음 진입 방식은 user gesture 가 **없으므로** muted 자동재생만 동작: - 페이지 새로고침 (F5) - URL 직접 입력 - 새 탭으로 열기 (`target="_blank"`) - 북마크 / 히스토리 진입 - iframe `allow="autoplay"` 미지정 - 다음 경우는 user gesture 가 **있어** 음소거 없이 자동재생 가능 (브라우저별 차이 있음): - 동일 origin 링크 클릭으로 진입 (Chrome/Edge 전파, Safari/Firefox 보수적) - 사용자 클릭/탭 이후 `play()` 호출 - iOS Safari 는 어느 경우에도 unmuted 자동재생 불가 (항상 user gesture 필수) **브라우저별 정책 요약**: - Chrome/Edge Desktop: MEI(Media Engagement Index) 기반. 자주 방문 사이트는 자동 허용. 신규 방문자는 muted 만 허용. - Chrome Mobile: muted 자동재생만 항상 허용. unmuted 는 user gesture 필수. - Safari Desktop: 자주 방문 사이트 자동 허용. 그 외 user gesture 필수. - Safari iOS: muted + `playsInline` 만 자동재생 허용. unmuted 는 **항상** user gesture 필수. - Firefox: muted 자동재생 허용. unmuted 는 user gesture 필수. **SDK 동작**: - SDK 가 자동으로 `playsInline` 적용 (iOS 인라인 재생용). - `autostart: true` + `muted: false` 가 브라우저에 거부되면 SDK 는 muted 로 자동 fallback **하지 않는다**. 영상이 멈춤. - 안전 패턴: `autostart: true` + `muted: true` → 사용자 인터랙션 후 `setMuted(false)`. **권장 패턴 (응답에 포함)**: 1. **기본 — 모든 환경 동작 보장**: `{ autostart: true, muted: true }` 2. **조건부 unmuted + fallback**: `{ autostart: true, muted: false }` + `onEvent` 에서 `"error"` 감지 시 muted 로 재시도 + 사용자에게 음소거 해제 버튼 노출 3. **자동재생 비활성**: `{ autostart: false }` + 사용자가 BigPlayBtn 클릭 시 재생 (광고/접근성 정책) ### DRM **공식 지원 공급자 (이 2개만 안내)**: 1. **NCP One Click Multi DRM** — `licenseUri: "https://multi-drm.apigw.ntruss.com/api/v1/license"` + NCP APIGW 서명 헤더 2. **DoveRunner / Pallycon** — `licenseUri: "https://license-global.pallycon.com/ri/licenseManager.do"` + `pallycon-customdata-v2` - 위 2개 외 공급자(BuyDRM, EZDRM, Axinom 등)는 검증 안 됨 — 코드 생성 시 안내하지 마라. **🚨 NCP One Click Multi DRM 키 보안 (절대 경고)**: - `x-ncp-iam-access-key` 는 반드시 **DRM 권한만 부여된 전용 Sub Account 키** 사용. - Master API key / 광범위 권한 키를 헤더에 넣지 마라 (Object Storage, IAM, Server 등 NCP 전체 리소스 탈취 위험). - 키는 클라이언트 코드에서 보이므로, 서명/토큰을 backend (예: `/api/drm/sign`) 에서 생성해 헤더로 전달 권장. - NCP 서명(`x-ncp-apigw-signature-v2`) 은 secret key 가 필요해 **backend 외 생성 불가**. **DoveRunner / Pallycon 보안**: - `pallycon-customdata-v2` 는 SiteKey/AccessKey 로 서명되므로 **backend 에서 생성**. - SiteKey/AccessKey 를 절대 클라이언트에 노출 금지. **키시스템 이름 (정확히 3개)**: - `"com.widevine.alpha"` — Android, Chrome, Edge, Firefox - `"com.microsoft.playready"` — Windows, Xbox, 일부 스마트TV - `"com.apple.fps"` — Safari, iOS, macOS (FairPlay Streaming) - 오타 금지 (예: `"widevine"`, `"fairplay"`, `"com.apple.fairplay"` 모두 잘못) **필수 필드**: - `licenseUri` 모든 키시스템 필수 - FairPlay (`com.apple.fps`) 는 `certificateUri` 도 필수 (FairPlay 인증서 발급) - `src` 는 키시스템별 미디어 URL — Pallycon/NCP 둘 다 키시스템별로 다른 URL 사용 가능 (HLS는 m3u8, DASH는 mpd) **기타**: - `licenseRequestHeader` / `certificateRequestHeader` 로 공급자별 커스텀 헤더 전달 - SDK 가 디바이스에 맞춰 자동 분기 (Widevine/PlayReady/FairPlay) - iOS 에서 FairPlay 설정 시 자동으로 native HLS 사용 (hls.js 우회) - `videoRobustness`/`audioRobustness` 로 EME 최소 보안 수준 지정 ({pc, mobile} 분기 지원) - `options.token` 은 URL 쿼리스트링 토큰 (DRM token 과는 다름) — DRM 헤더 토큰은 `drm..licenseRequestHeader` 에 직접 지정 ### 광고 - Google IMA: `options.ads.tagUrl` (VAST/VMAP) — 공식 지원 - IMA SDK 는 외부에서 자동 로드됨 (별도 import 불필요) - iOS 에서 IMA: `setDisableCustomPlaybackForIOS10Plus(true)` 자동 적용 (SDK 내부 처리) - 광고 클릭 → 외부 페이지 + 광고 일시정지, 다시 광고 영역 클릭 시 resume (SDK 내부 처리) - 광고 이벤트 흐름: `adStart` → `adComplete` 또는 `adSkip` → 콘텐츠 자동 재개 - VAST 광고 없음/에러 → `adError` 발사 + 콘텐츠 자동 재생 - 자막은 광고 재생 중 자동 숨김 ### OTT 특화 기능 **구간 스킵 (Intro / Opening / Ending)**: - `playlist[].intro` / `playlist[].opening` / `playlist[].ending` 각각 `{ start, duration }` 형태 - `start` 는 `"HH:MM:SS"` 또는 `"MM:SS"` 문자열, `duration` 은 초 단위 number - 재생 시간이 해당 구간에 진입하면 SDK 가 자동으로 스킵 버튼 노출 - 클릭 시 `start + duration` 시점으로 점프 (예: `intro: { start: "00:00:00", duration: 10 }` → 10초로 점프) - `ending` 은 "다음 화" 추천에 자주 사용 (override.nextSource 와 함께) **연령등급 (ageRating)**: - `playlist[].ageRating`: `"all"` (전체관람가) | `"12"` | `"15"` | `"19"` (청소년관람불가) | 커스텀 문자열 - 영상 진입 시점에 연령등급 오버레이 자동 노출 - 한국 방송통신위원회 5단계 기준 따름 (커스텀 문자열도 허용) **콘텐츠 경고 (contentWarnings)**: - `playlist[].contentWarnings: string[]` — 복수 지정 가능 - 가능한 값: `"sexuality"` (선정성) / `"violence"` (폭력성) / `"language"` (언어) / `"drugs"` (약물) / `"horror"` (공포) / `"imitation"` (모방위험) / `"provocative"` (자극성) - ageRating 과 함께 진입 안내 오버레이에 표시됨 - 커스텀 문자열도 허용 **시리즈/다음화 처리**: - `options.override.nextSource` / `options.override.prevSource` 콜백으로 컨트롤바의 next/prev 버튼 동작 정의 - `ended` 이벤트로 자동 다음화 처리 - 시리즈는 보통 1개 episode 당 1 playlist 항목 + 상태로 idx 관리 **메타데이터 (description)**: - `playlist[].description.title` — 타이틀 (string 또는 `{ ko, en, ja }` 다국어 객체) - `playlist[].description.profile_name` — 스튜디오/채널명 - `playlist[].description.profile_image` — 스튜디오/채널 로고 URL - `playlist[].description.created_at` — 게시일 (자유 포맷) **조합 권장 패턴**: - 시리즈물 1편당: file + poster + description + ageRating + contentWarnings + intro/opening/ending + vtt (다국어) + drm - 시리즈물 전체: 위 항목을 episode 배열로 관리하고 상태로 idx 변경 ### 자막 (Caption) **역할 분리 (혼동 금지)**: - `playlist[].vtt[]` → ★ **외부 VTT 파일 등록** (자막 소스). 이게 없으면 자막 자체가 없음. - `captionType` → 렌더링 **방식** 선택 ("html" 기본 / "native"). 자막 등록과 무관. - `captionStyle` → 자막 텍스트의 **시각적 스타일**. 자막 등록과 무관. **소스 등록 위치**: - 외부 VTT 자막은 **`options.playlist[].vtt[]` 배열에만** 등록한다. - `options.subtitles` / `options.captions` / `options.vtt` / `options.tracks` 같은 옵션은 없다. - `` HTML 요소는 children 으로 지정해도 안 먹는다. **vtt[] 필드**: - `src` 필수 — .vtt 파일 URL (V1 호환: `file` 도 가능, src 없을 때만 자동 정규화) - `id` — lang code (예: "ko", "en", "ja", "zh") - `label` — 자막 선택 메뉴에 표시되는 텍스트 (예: "한국어") - `default: true` — 초기 자동 선택. 배열에서 1개만 지정해야 함. **captionType 동작**: - `"html"` (기본) — div 오버레이로 자막 렌더링. `captionStyle` 전체 적용. OS 접근성 자막 설정 무시. 모바일 이중 자막 방지를 위해 `track.mode="hidden"` 강제됨. - `"native"` — 브라우저 기본 `::cue` 렌더링. OS 접근성 자막 설정 따라감 (iOS Settings > 손쉬운 사용 > 자막 / Android 접근성 > 자막 환경설정). **`captionStyle` 무시됨**. **captionStyle 값 규칙**: - 반응형 값(`fontSize`/`lineHeight`/`bottomOffset`/`sidePadding`)은 단일값 또는 `{ pc, mobile }` 객체. 배열·"desktop/phone" 키는 안 됨. - `edgeStyle` 은 `"none" | "dropshadow" | "raised" | "depressed" | "uniform"` 5개만 (오타·임의값 금지). - 미지원 CSS 속성(border, boxShadow, margin, fontWeight 등)은 captionStyle 에 넣지 마라. - `backgroundColor: "transparent"` (기본) → 박스 배경 없이 텍스트만. - `padding` 은 CSS padding 문자열 또는 숫자 (예: `"3px 5px"`, `5`). **Ref 메소드 (자막 ON/OFF, 선택)**: - `playerRef.current?.toggleSubtitle()` — ON ↔ OFF 토글 - `playerRef.current?.setSubtitleEnabled(boolean)` — 명시적 ON/OFF - `playerRef.current?.selectSubTitle(idx)` — vtt 배열 인덱스로 자막 변경 - `addSubtitle` / `loadSubtitle` / `setSubtitleSrc` / `changeCaption` 같은 메소드는 **존재하지 않는다**. - 자막 소스 자체를 동적으로 교체하려면 `options.playlist` 의 vtt 배열을 새로 만들어 props 로 전달 (React 가 다시 마운트). **이벤트**: - `subtitleChange` — 자막 선택이 바뀔 때 발사 (Ref 의 selectSubTitle 또는 사용자 메뉴 클릭 시) ### 이벤트 - 단일 `onEvent` 콜백, `e.type` 으로 분기 (switch 권장) - Ref 의 `on(type, cb)` 로 특정 타입만 구독 가능, 반환값으로 해제 - 콜백은 useCallback 으로 안정된 참조 유지 권장 - `*` 타입은 모든 이벤트 받음 ### 보안 - accessKey 는 **Sub Account** 의 것만 사용 (Master API key 금지) - DRM source URL / token 은 **backend 에서 생성**해 전달 - 워터마크: 사용자 식별 정보 (예: 이메일) 를 `watermarkText` 에 → 콘텐츠 무단 캡처 시 추적 - platform: `"gov"` (공공) 사용 시 추가 IP 제한 정책 확인 ### Platform / URL - `platform: "pub"` (민간, 기본) — `https://player.vpe.naverncp.com/v2/ncplayer.js` - `platform: "gov"` (공공) — `https://player.vpe.gov-ntruss.com/v2/ncplayer.js` - `stage: "prod"` (기본) — 운영, `stage: "stg"` — 스테이징 - UMD 스크립트의 `?access_key=...` 쿼리스트링에서 accessKey 자동 추출 ### 환경 식별 (LLM) - "Next.js" / "App Router" / "Pages Router" / "use client" / "app/" 경로 → Next.js - "Vite" / "CRA" / "create-react-app" / "vite.config" → React (Vite/CRA) - "Expo" / "React Native" / "@react-native" → 별도 패키지 안내 - "PHP" / "JSP" / "ASP" / "