From 2090753a4f0ec5b65e6aadcbf2fa48f37e2b1555 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Wed, 18 Jun 2025 00:30:52 +0900 Subject: [PATCH 1/3] feat: add dynamic progressInterval support --- example/src/App.tsx | 11 ++++++++++ src/YoutubePlayer.tsx | 31 ++++++++++++++++++--------- src/YoutubePlayer.web.tsx | 20 ++++++++++++++++- src/hooks/useCreateLocalPlayerHtml.ts | 15 +++++++++++-- src/hooks/youtubeIframeScripts.ts | 4 ++-- src/types/message.ts | 25 +++++++++++++++++++++ src/types/youtube.ts | 10 +++++++++ 7 files changed, 101 insertions(+), 15 deletions(-) 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 +122,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 +227,12 @@ const YoutubePlayer = forwardRef( }; }, [isReady, sendCommand]); + useEffect(() => { + if (isReady) { + sendCommand('updateProgressInterval', [progressInterval]); + } + }, [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..37dc744 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, @@ -39,6 +40,8 @@ const YoutubePlayer = forwardRef( const createPlayerRef = useRef<() => void>(null); const progressInterval = useRef(null); + const intervalRef = useRef(interval); + const stopProgressTracking = useCallback(() => { if (progressInterval.current) { clearInterval(progressInterval.current); @@ -47,6 +50,10 @@ const YoutubePlayer = forwardRef( }, []); const startProgressTracking = useCallback(() => { + if (!intervalRef.current) { + return; + } + if (progressInterval.current) { clearInterval(progressInterval.current); } @@ -73,7 +80,7 @@ const YoutubePlayer = forwardRef( console.error('Progress tracking error:', error); stopProgressTracking(); } - }, 1000); + }, intervalRef.current); }, [onProgress, stopProgressTracking]); const loadYouTubeAPI = useCallback(() => { @@ -229,6 +236,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..94172ce 100644 --- a/src/hooks/useCreateLocalPlayerHtml.ts +++ b/src/hooks/useCreateLocalPlayerHtml.ts @@ -171,8 +171,19 @@ const useCreateLocalPlayerHtml = ({ }, setSize: (width, height) => player && player.setSize(width, height), - - cleanup: cleanup + updateProgressInterval: (newInterval) => { + window.currentInterval = newInterval; + + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + } + + if (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..2bdb522 100644 --- a/src/hooks/youtubeIframeScripts.ts +++ b/src/hooks/youtubeIframeScripts.ts @@ -1,6 +1,6 @@ const startProgressTracking = /* js */ ` function startProgressTracking() { - if (isDestroyed) { + if (isDestroyed || !window.currentInterval) { 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; From ca6e1d81b1e10f52f3fd6e1c395caedd39bd3000 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Wed, 18 Jun 2025 00:39:04 +0900 Subject: [PATCH 2/3] docs: document progressInterval and onProgress callback behavior --- README-ko_kr.md | 4 +++- README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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 ( ) From 6a57e46f70cba400b990b1af470e33fe27d3964f Mon Sep 17 00:00:00 2001 From: saseungmin Date: Wed, 18 Jun 2025 09:26:51 +0900 Subject: [PATCH 3/3] refactor: safe number for progress interval --- src/YoutubePlayer.tsx | 5 ++++- src/YoutubePlayer.web.tsx | 3 +-- src/hooks/useCreateLocalPlayerHtml.ts | 6 ++++-- src/hooks/youtubeIframeScripts.ts | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/YoutubePlayer.tsx b/src/YoutubePlayer.tsx index 41f0fd5..cbe8b2b 100644 --- a/src/YoutubePlayer.tsx +++ b/src/YoutubePlayer.tsx @@ -5,6 +5,7 @@ import YoutubePlayerWrapper from './YoutubePlayerWrapper'; import useCreateLocalPlayerHtml from './hooks/useCreateLocalPlayerHtml'; import type { CommandType, MessageData } from './types/message'; import type { PlayerControls, YoutubePlayerProps } from './types/youtube'; +import { safeNumber } from './utils/validate'; const { width: screenWidth } = Dimensions.get('window'); @@ -229,7 +230,9 @@ const YoutubePlayer = forwardRef( useEffect(() => { if (isReady) { - sendCommand('updateProgressInterval', [progressInterval]); + const safeInterval = safeNumber(progressInterval); + + sendCommand('updateProgressInterval', [safeInterval]); } }, [progressInterval, isReady, sendCommand]); diff --git a/src/YoutubePlayer.web.tsx b/src/YoutubePlayer.web.tsx index 37dc744..1c42290 100644 --- a/src/YoutubePlayer.web.tsx +++ b/src/YoutubePlayer.web.tsx @@ -39,8 +39,7 @@ const YoutubePlayer = forwardRef( const containerRef = useRef(null); const createPlayerRef = useRef<() => void>(null); const progressInterval = useRef(null); - - const intervalRef = useRef(interval); + const intervalRef = useRef(interval); const stopProgressTracking = useCallback(() => { if (progressInterval.current) { diff --git a/src/hooks/useCreateLocalPlayerHtml.ts b/src/hooks/useCreateLocalPlayerHtml.ts index 94172ce..56a8c41 100644 --- a/src/hooks/useCreateLocalPlayerHtml.ts +++ b/src/hooks/useCreateLocalPlayerHtml.ts @@ -172,14 +172,16 @@ const useCreateLocalPlayerHtml = ({ setSize: (width, height) => player && player.setSize(width, height), updateProgressInterval: (newInterval) => { - window.currentInterval = newInterval; + const interval = Number(newInterval) > 0 ? Number(newInterval) : null; + + window.currentInterval = interval; if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } - if (player && player.getPlayerState() === YT.PlayerState.PLAYING) { + if (interval && player && player.getPlayerState() === YT.PlayerState.PLAYING) { startProgressTracking(); } }, diff --git a/src/hooks/youtubeIframeScripts.ts b/src/hooks/youtubeIframeScripts.ts index 2bdb522..0becb1a 100644 --- a/src/hooks/youtubeIframeScripts.ts +++ b/src/hooks/youtubeIframeScripts.ts @@ -1,6 +1,6 @@ const startProgressTracking = /* js */ ` function startProgressTracking() { - if (isDestroyed || !window.currentInterval) { + if (isDestroyed || typeof window.currentInterval !== 'number' || window.currentInterval <= 0) { return; }