diff --git a/README-ko_kr.md b/README-ko_kr.md index aeb20b0..453aa47 100644 --- a/README-ko_kr.md +++ b/README-ko_kr.md @@ -212,10 +212,11 @@ function App() { ### 유용한 기능 #### 재생 진행률 추적 +- `progressInterval`이 설정된 경우, 해당 간격(ms)마다 `onProgress` 콜백이 호출됩니다. +- `progressInterval`이 `undefined`이거나 `0` 또는 `null`인 경우, `onProgress` 콜백은 호출되지 않습니다. ```tsx function App() { - // 1초마다 호출되는 진행률 이벤트 콜백 const handleProgress = useCallback((progress: ProgressData) => { setCurrentTime(progress.currentTime); setDuration(progress.duration); @@ -225,6 +226,7 @@ function App() { return ( ) diff --git a/README.md b/README.md index e55edfc..d84eb8e 100644 --- a/README.md +++ b/README.md @@ -212,10 +212,11 @@ function App() { ### Useful Features #### Playback Progress Tracking +- If `progressInterval` is provided, the `onProgress` callback will be invoked at the specified interval (in milliseconds). +- If `progressInterval` is `undefined`, `0`, or `null`, progress tracking is disabled and `onProgress` will not be called. ```tsx function App() { - // Progress event callback called every second const handleProgress = useCallback((progress: ProgressData) => { setCurrentTime(progress.currentTime); setDuration(progress.duration); @@ -225,6 +226,7 @@ function App() { return ( ) diff --git a/example/src/App.tsx b/example/src/App.tsx index 6298305..8010420 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -26,6 +26,7 @@ function App() { const [volume, setVolume] = useState(100); const [isMuted, setIsMuted] = useState(false); const [videoId, setVideoId] = useState('AbZH7XWDW_k'); + const [progressInterval, setProgressInterval] = useState(1000); const handleReady = useCallback((playerInfo: PlayerInfo) => { console.log('Player is ready!'); @@ -183,6 +184,7 @@ function App() { rel: false, muted: true, }} + progressInterval={progressInterval} onReady={handleReady} onStateChange={handleStateChange} onProgress={handleProgress} @@ -216,6 +218,15 @@ function App() { 버퍼: {(loadedFraction * 100).toFixed(1)}% + + setProgressInterval(progressInterval === 0 ? 1000 : 0)} + > + {progressInterval}ms interval + + + ( videoId, width = screenWidth, height = 200, + progressInterval, onReady, onStateChange, onError, @@ -121,8 +123,12 @@ const YoutubePlayer = forwardRef( ); const sendCommand = useCallback( - // biome-ignore lint/suspicious/noExplicitAny: - (command: string, args: (string | number | boolean | undefined)[] = [], needsResult = false): Promise => { + ( + command: CommandType, + args: (string | number | boolean | undefined)[] = [], + needsResult = false, + // biome-ignore lint/suspicious/noExplicitAny: + ): Promise => { return new Promise((resolve) => { if (!webViewRef.current || !isReady) { resolve(null); @@ -222,6 +228,14 @@ const YoutubePlayer = forwardRef( }; }, [isReady, sendCommand]); + useEffect(() => { + if (isReady) { + const safeInterval = safeNumber(progressInterval); + + sendCommand('updateProgressInterval', [safeInterval]); + } + }, [progressInterval, isReady, sendCommand]); + return ( ( style={[styles.webView, webViewStyle]} onMessage={handleMessage} {...webviewProps} - javaScriptEnabled={true} - originWhitelist={['*']} - domStorageEnabled={true} - mediaPlaybackRequiresUserAction={false} - allowsInlineMediaPlayback={true} - allowsFullscreenVideo={true} - scrollEnabled={false} + javaScriptEnabled + domStorageEnabled + allowsFullscreenVideo + allowsInlineMediaPlayback bounces={false} + scrollEnabled={false} + mediaPlaybackRequiresUserAction={false} + originWhitelist={['*']} onError={(error) => { console.error('WebView error:', error); onError?.({ code: -1, message: 'WebView loading error' }); diff --git a/src/YoutubePlayer.web.tsx b/src/YoutubePlayer.web.tsx index 0928cba..1c42290 100644 --- a/src/YoutubePlayer.web.tsx +++ b/src/YoutubePlayer.web.tsx @@ -10,6 +10,7 @@ const YoutubePlayer = forwardRef( videoId, width, height = 200, + progressInterval: interval, onReady, onStateChange, onError, @@ -38,6 +39,7 @@ const YoutubePlayer = forwardRef( const containerRef = useRef(null); const createPlayerRef = useRef<() => void>(null); const progressInterval = useRef(null); + const intervalRef = useRef(interval); const stopProgressTracking = useCallback(() => { if (progressInterval.current) { @@ -47,6 +49,10 @@ const YoutubePlayer = forwardRef( }, []); const startProgressTracking = useCallback(() => { + if (!intervalRef.current) { + return; + } + if (progressInterval.current) { clearInterval(progressInterval.current); } @@ -73,7 +79,7 @@ const YoutubePlayer = forwardRef( console.error('Progress tracking error:', error); stopProgressTracking(); } - }, 1000); + }, intervalRef.current); }, [onProgress, stopProgressTracking]); const loadYouTubeAPI = useCallback(() => { @@ -229,6 +235,17 @@ const YoutubePlayer = forwardRef( } }, [videoId, createPlayer]); + useEffect(() => { + intervalRef.current = interval; + + if (interval) { + startProgressTracking(); + return; + } + + stopProgressTracking(); + }, [interval, startProgressTracking, stopProgressTracking]); + const play = useCallback(() => { playerRef.current?.playVideo(); }, []); diff --git a/src/hooks/useCreateLocalPlayerHtml.ts b/src/hooks/useCreateLocalPlayerHtml.ts index 9212e9d..56a8c41 100644 --- a/src/hooks/useCreateLocalPlayerHtml.ts +++ b/src/hooks/useCreateLocalPlayerHtml.ts @@ -171,8 +171,21 @@ const useCreateLocalPlayerHtml = ({ }, setSize: (width, height) => player && player.setSize(width, height), - - cleanup: cleanup + updateProgressInterval: (newInterval) => { + const interval = Number(newInterval) > 0 ? Number(newInterval) : null; + + window.currentInterval = interval; + + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + } + + if (interval && player && player.getPlayerState() === YT.PlayerState.PLAYING) { + startProgressTracking(); + } + }, + cleanup: cleanup, }; window.addEventListener('message', function(event) { diff --git a/src/hooks/youtubeIframeScripts.ts b/src/hooks/youtubeIframeScripts.ts index 724bac3..0becb1a 100644 --- a/src/hooks/youtubeIframeScripts.ts +++ b/src/hooks/youtubeIframeScripts.ts @@ -1,6 +1,6 @@ const startProgressTracking = /* js */ ` function startProgressTracking() { - if (isDestroyed) { + if (isDestroyed || typeof window.currentInterval !== 'number' || window.currentInterval <= 0) { return; } @@ -31,7 +31,7 @@ const startProgressTracking = /* js */ ` console.error('Progress tracking error:', error); stopProgressTracking(); } - }, 1000); + }, window.currentInterval); } `; diff --git a/src/types/message.ts b/src/types/message.ts index 913f98c..f4a9be7 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -10,6 +10,31 @@ export type MessageType = | 'autoplayBlocked' | 'commandResult'; +export type CommandType = + | 'play' + | 'pause' + | 'stop' + | 'seekTo' + | 'setVolume' + | 'getVolume' + | 'mute' + | 'unMute' + | 'isMuted' + | 'getCurrentTime' + | 'getDuration' + | 'getVideoUrl' + | 'getVideoEmbedCode' + | 'getPlaybackRate' + | 'setPlaybackRate' + | 'getAvailablePlaybackRates' + | 'getPlayerState' + | 'getVideoLoadedFraction' + | 'loadVideoById' + | 'cueVideoById' + | 'setSize' + | 'cleanup' + | 'updateProgressInterval'; + interface ReadyMessageData { type: 'ready'; playerInfo: PlayerInfo; diff --git a/src/types/youtube.ts b/src/types/youtube.ts index f3198cb..3ca971d 100644 --- a/src/types/youtube.ts +++ b/src/types/youtube.ts @@ -22,6 +22,12 @@ export type YoutubePlayerProps = { videoId: string; width?: DimensionValue; height?: DimensionValue; + /** + * @description The interval (in milliseconds) at which `onProgress` callback is called. + * Must be a positive number to enable progress tracking. + * If not provided or set to 0/falsy value, progress tracking is disabled. + */ + progressInterval?: number; style?: StyleProp; /** * @platform ios, android @@ -40,6 +46,10 @@ export type YoutubePlayerProps = { onReady?: (playerInfo: PlayerInfo) => void; onStateChange?: (state: PlayerState) => void; onError?: (error: YouTubeError) => void; + /** + * @description Callback function called at the specified `progressInterval`. + * Only invoked when `progressInterval` is provided as a positive number. + */ onProgress?: (progress: ProgressData) => void; onPlaybackRateChange?: (playbackRate: number) => void; onPlaybackQualityChange?: (quality: string) => void;