From 0e16cb8d7810d13a82a2299ebbceb0f036324477 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Mon, 16 Jun 2025 17:56:54 +0900 Subject: [PATCH 1/4] fix: youTube player state synchronization issues (onReady, mute) --- example/src/App.tsx | 92 ++++++++++++++++++++----------- src/YoutubePlayer.tsx | 15 ++++- src/YoutubePlayer.web.tsx | 17 +++++- src/hooks/youtubeIframeScripts.ts | 3 +- src/index.tsx | 1 + src/types/iframe.d.ts | 22 +++++--- src/types/message.ts | 5 +- src/types/youtube.ts | 18 +++++- 8 files changed, 124 insertions(+), 49 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 6657a37..f54a1ac 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -6,8 +6,15 @@ import { type ProgressData, type YouTubeError, YoutubePlayer, + type PlayerInfo, } from 'react-native-youtube-bridge'; +const formatTime = (seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +}; + function App() { const playerRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); @@ -20,19 +27,25 @@ function App() { const [isMuted, setIsMuted] = useState(false); const [videoId, setVideoId] = useState('AbZH7XWDW_k'); - const handleReady = useCallback(async () => { + const handleReady = useCallback(async (playerInfo: PlayerInfo) => { console.log('Player is ready!'); Alert.alert('알림', 'YouTube 플레이어가 준비되었습니다!'); // 플레이어 준비 완료 후 정보 가져오기 try { - const rates = await playerRef.current?.getAvailablePlaybackRates(); - const vol = await playerRef.current?.getVolume(); - const muted = await playerRef.current?.isMuted(); - - if (rates) setAvailableRates(rates); - if (vol !== undefined) setVolume(vol); - if (muted !== undefined) setIsMuted(muted); + console.log('rates', playerInfo.availablePlaybackRates); + console.log('vol', playerInfo.volume); + console.log('muted', playerInfo.muted); + + if (playerInfo.availablePlaybackRates) { + setAvailableRates(playerInfo.availablePlaybackRates); + } + if (playerInfo.volume !== undefined) { + setVolume(playerInfo.volume); + } + if (playerInfo.muted !== undefined) { + setIsMuted(playerInfo.muted); + } } catch (error) { console.error('Error getting player info:', error); } @@ -90,42 +103,55 @@ function App() { Alert.alert('알림', '자동재생이 브라우저에 의해 차단되었습니다'); }, []); - const formatTime = (seconds: number): string => { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; - }; - const changePlaybackRate = (rate: number) => { playerRef.current?.setPlaybackRate(rate); }; - const changeVolume = async (newVolume: number) => { + const changeVolume = (newVolume: number) => { playerRef.current?.setVolume(newVolume); setVolume(newVolume); }; - const toggleMute = async () => { + const toggleMute = useCallback(() => { if (isMuted) { playerRef.current?.unMute(); - } else { - playerRef.current?.mute(); + return; } - const muted = await playerRef.current?.isMuted(); - if (muted !== undefined) setIsMuted(muted); - }; + + playerRef.current?.mute(); + + setIsMuted(!isMuted); + }, [isMuted]); + + const onPlay = useCallback(() => { + if (isPlaying) { + playerRef.current?.pause(); + return; + } + + playerRef.current?.play(); + }, [isPlaying]); const getPlayerInfo = async () => { try { - const [currentTime, duration, url, _, state, loaded] = await Promise.all([ + const [currentTime, duration, url, state, loaded] = await Promise.all([ playerRef.current?.getCurrentTime(), playerRef.current?.getDuration(), playerRef.current?.getVideoUrl(), - playerRef.current?.getVideoEmbedCode(), playerRef.current?.getPlayerState(), playerRef.current?.getVideoLoadedFraction(), ]); + console.log( + ` + currentTime: ${currentTime} + duration: ${duration} + url: ${url} + state: ${state} + loaded: ${loaded} + `, + ); + Alert.alert( '플레이어 정보', `현재 시간: ${formatTime(currentTime || 0)}\n` + @@ -151,7 +177,6 @@ function App() { @@ -187,15 +215,13 @@ function App() { { - if (isPlaying) { - playerRef.current?.pause(); - } else { - playerRef.current?.play(); - } - }} + style={[styles.button, styles.seekButton]} + onPress={() => playerRef.current?.seekTo(currentTime > 10 ? currentTime - 10 : 0)} > + ⏪ -10초 + + + {isPlaying ? '⏸️ 일시정지' : '▶️ 재생'} @@ -205,7 +231,7 @@ function App() { playerRef.current?.seekTo(currentTime + 10)} + onPress={() => playerRef.current?.seekTo(currentTime + 10, true)} > ⏭️ +10초 diff --git a/src/YoutubePlayer.tsx b/src/YoutubePlayer.tsx index 28f8548..b54c00b 100644 --- a/src/YoutubePlayer.tsx +++ b/src/YoutubePlayer.tsx @@ -51,8 +51,21 @@ const YoutubePlayer = forwardRef( console.log('handleMessage', data); if (data.type === 'ready') { + const { playerInfo } = data; + setIsReady(true); - onReady?.(); + onReady?.({ + availablePlaybackRates: playerInfo.availablePlaybackRates, + availableQualityLevels: playerInfo.availableQualityLevels, + currentTime: playerInfo.currentTime, + duration: playerInfo.duration, + muted: playerInfo.muted, + playbackQuality: playerInfo.playbackQuality, + playbackRate: playerInfo.playbackRate, + playerState: playerInfo.playerState, + size: playerInfo.size, + volume: playerInfo.volume, + }); return; } diff --git a/src/YoutubePlayer.web.tsx b/src/YoutubePlayer.web.tsx index 9d05a3f..1841e8c 100644 --- a/src/YoutubePlayer.web.tsx +++ b/src/YoutubePlayer.web.tsx @@ -144,8 +144,21 @@ const YoutubePlayer = forwardRef( enablejsapi: 1, }, events: { - onReady: () => { - onReady?.(); + onReady: (event) => { + const { playerInfo } = event.target; + + onReady?.({ + availablePlaybackRates: playerInfo.availablePlaybackRates, + availableQualityLevels: playerInfo.availableQualityLevels, + currentTime: playerInfo.currentTime, + duration: playerInfo.duration, + muted: playerInfo.muted, + playbackQuality: playerInfo.playbackQuality, + playbackRate: playerInfo.playbackRate, + playerState: playerInfo.playerState, + size: playerInfo.size, + volume: playerInfo.volume, + }); startProgressTracking(); }, onStateChange: (event) => { diff --git a/src/hooks/youtubeIframeScripts.ts b/src/hooks/youtubeIframeScripts.ts index d471d1a..0edc7d9 100644 --- a/src/hooks/youtubeIframeScripts.ts +++ b/src/hooks/youtubeIframeScripts.ts @@ -53,8 +53,7 @@ const onPlayerReady = /* js */ ` try { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready', - duration: player.getDuration(), - availablePlaybackRates: player.getAvailablePlaybackRates() + playerInfo: event.target.playerInfo })); startProgressTracking(); } catch (error) { diff --git a/src/index.tsx b/src/index.tsx index b55c636..28b6367 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,4 +8,5 @@ export { type PlaybackQuality, type PlayerControls, type YouTubeError, + type PlayerInfo, } from './types/youtube'; diff --git a/src/types/iframe.d.ts b/src/types/iframe.d.ts index 5417ab1..588cede 100644 --- a/src/types/iframe.d.ts +++ b/src/types/iframe.d.ts @@ -1,4 +1,4 @@ -import type { ERROR_CODES, PlaybackQuality, PlayerState, YouTubeError } from './youtube'; +import type { ERROR_CODES, PlaybackQuality, PlayerInfo, PlayerState, YouTubeError } from './youtube'; declare global { interface Window { @@ -17,6 +17,14 @@ export type EventType = | 'onApiChange' | 'onAutoplayBlocked'; +export type PlayerEvent = { + target: YouTubePlayer & { + options: Options; + playerInfo: PlayerInfo; + }; + data: T; +}; + export interface Options { width?: number | string | undefined; height?: number | string | undefined; @@ -48,12 +56,12 @@ export interface Options { | undefined; events?: | { - onReady?: (event: CustomEvent) => void; - onStateChange?: (event: { data: PlayerState }) => void; - onPlaybackQualityChange?: (event: { data: PlaybackQuality }) => void; - onPlaybackRateChange?: (event: { data: number }) => void; - onError?: (event: { data: keyof typeof ERROR_CODES }) => void; - onAutoplayBlocked?: (event: CustomEvent) => void; + onReady?: (event: PlayerEvent) => void; + onStateChange?: (event: PlayerEvent) => void; + onPlaybackQualityChange?: (event: PlayerEvent) => void; + onPlaybackRateChange?: (event: PlayerEvent) => void; + onError?: (event: PlayerEvent) => void; + onAutoplayBlocked?: (event: PlayerEvent) => void; } | undefined; } diff --git a/src/types/message.ts b/src/types/message.ts index 6942042..913f98c 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,4 +1,4 @@ -import type { PlaybackQuality, PlayerState, ProgressData, YouTubeError } from './youtube'; +import type { PlaybackQuality, PlayerInfo, PlayerState, ProgressData, YouTubeError } from './youtube'; export type MessageType = | 'ready' @@ -12,8 +12,7 @@ export type MessageType = interface ReadyMessageData { type: 'ready'; - duration: number; - availablePlaybackRates: number[]; + playerInfo: PlayerInfo; } interface StateChangeMessageData { diff --git a/src/types/youtube.ts b/src/types/youtube.ts index 4ebba34..1e28b4a 100644 --- a/src/types/youtube.ts +++ b/src/types/youtube.ts @@ -19,7 +19,7 @@ export type YoutubePlayerProps = { height?: number | `${number}%`; style?: StyleProp; // Events - onReady?: () => void; + onReady?: (playerInfo: PlayerInfo) => void; onStateChange?: (state: PlayerState) => void; onError?: (error: YouTubeError) => void; onProgress?: (progress: ProgressData) => void; @@ -53,6 +53,22 @@ export const ERROR_CODES = { export type PlaybackQuality = 'small' | 'medium' | 'large' | 'hd720' | 'hd1080' | 'highres'; +export type PlayerInfo = { + availablePlaybackRates?: number[]; + availableQualityLevels?: PlaybackQuality[]; + currentTime?: number; + duration?: number; + muted?: boolean; + playbackQuality?: PlaybackQuality; + playbackRate?: number; + playerState?: PlayerState; + size?: { + width: number; + height: number; + }; + volume?: number; +}; + export type ProgressData = { currentTime: number; duration: number; From 861928ec429eae6cd9f9603a42abc126e8513f60 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Mon, 16 Jun 2025 17:59:17 +0900 Subject: [PATCH 2/4] refactor: import sort --- example/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index f54a1ac..0224c1d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,11 +2,11 @@ import { useCallback, useRef, useState } from 'react'; import { Alert, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { type PlayerControls, + type PlayerInfo, PlayerState, type ProgressData, type YouTubeError, YoutubePlayer, - type PlayerInfo, } from 'react-native-youtube-bridge'; const formatTime = (seconds: number): string => { From 0627bacc3f9d50b54fe7f5121501d77af9441458 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Mon, 16 Jun 2025 18:31:35 +0900 Subject: [PATCH 3/4] feat: webview style, iframe style props --- example/src/App.tsx | 6 +++++- src/YoutubePlayer.tsx | 5 ++++- src/YoutubePlayer.web.tsx | 3 ++- src/YoutubePlayerWrapper.tsx | 6 +++--- src/types/youtube.ts | 21 ++++++++++++++++++--- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 0224c1d..3dd2ede 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,5 +1,5 @@ import { useCallback, useRef, useState } from 'react'; -import { Alert, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Alert, Platform, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { type PlayerControls, type PlayerInfo, @@ -177,6 +177,7 @@ function App() { diff --git a/src/YoutubePlayer.tsx b/src/YoutubePlayer.tsx index b54c00b..21fd1ca 100644 --- a/src/YoutubePlayer.tsx +++ b/src/YoutubePlayer.tsx @@ -31,6 +31,8 @@ const YoutubePlayer = forwardRef( playsinline: true, rel: false, }, + webViewStyle, + webviewProps, }, ref, ) => { @@ -218,8 +220,9 @@ const YoutubePlayer = forwardRef( ( playsinline: true, rel: false, }, + iframeStyle, }, ref, ) => { const { startTime = 0, endTime, autoplay, controls, loop, playsinline, rel } = playerVars; - const { width: screenWidth } = useWindowDimensions(); const playerRef = useRef(null); @@ -390,6 +390,7 @@ const YoutubePlayer = forwardRef( style={{ width: '100%', height: '100%', + ...iframeStyle, }} /> diff --git a/src/YoutubePlayerWrapper.tsx b/src/YoutubePlayerWrapper.tsx index 1a5ecd1..2ab0dda 100644 --- a/src/YoutubePlayerWrapper.tsx +++ b/src/YoutubePlayerWrapper.tsx @@ -1,10 +1,10 @@ import type { ReactNode } from 'react'; -import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'; +import { type DimensionValue, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'; type YoutubePlayerWrapperProps = { children: ReactNode; - width?: number | `${number}%`; - height?: number | `${number}%`; + width?: DimensionValue; + height?: DimensionValue; style?: StyleProp; }; diff --git a/src/types/youtube.ts b/src/types/youtube.ts index 1e28b4a..50831e1 100644 --- a/src/types/youtube.ts +++ b/src/types/youtube.ts @@ -1,4 +1,6 @@ -import type { StyleProp, ViewStyle } from 'react-native'; +import type { CSSProperties } from 'react'; +import type { DimensionValue, StyleProp, ViewStyle } from 'react-native'; +import type { WebViewProps } from 'react-native-webview'; export type YoutubePlayerVars = { autoplay?: boolean; @@ -15,9 +17,22 @@ export type YoutubePlayerVars = { // YouTube IFrame API official documentation based export type YoutubePlayerProps = { videoId: string; - width?: number | `${number}%`; - height?: number | `${number}%`; + width?: DimensionValue; + height?: DimensionValue; style?: StyleProp; + /** + * @platform ios, android + */ + webViewStyle?: StyleProp; + /** + * @platform ios, android + */ + webviewProps?: Omit; + /** + * @platform web + */ + iframeStyle?: CSSProperties; + // Events onReady?: (playerInfo: PlayerInfo) => void; onStateChange?: (state: PlayerState) => void; From d8909d97f4308dfd64ed076663bf3fd09d9f22b5 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Mon, 16 Jun 2025 18:49:29 +0900 Subject: [PATCH 4/4] fix(example): toggle mute --- example/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 3dd2ede..5fa65a2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -115,12 +115,12 @@ function App() { const toggleMute = useCallback(() => { if (isMuted) { playerRef.current?.unMute(); + setIsMuted(false); return; } playerRef.current?.mute(); - - setIsMuted(!isMuted); + setIsMuted(true); }, [isMuted]); const onPlay = useCallback(() => {