diff --git a/.changeset/three-papayas-design.md b/.changeset/three-papayas-design.md new file mode 100644 index 0000000..73220b8 --- /dev/null +++ b/.changeset/three-papayas-design.md @@ -0,0 +1,11 @@ +--- +"react-native-youtube-bridge": minor +--- + +feat: add flexible source prop to support videoId and URL + +> [!note] +> ❗ BREAKING CHANGE: videoId prop replaced with source prop +> - source accepts string (videoId/URL) or object {videoId} | {url} +> - Add useYouTubeVideoId hook for internal parsing +> - Support multiple YouTube URL formats diff --git a/README-ko_kr.md b/README-ko_kr.md index ea67057..1a58ce6 100644 --- a/README-ko_kr.md +++ b/README-ko_kr.md @@ -40,7 +40,11 @@ import { YoutubePlayer } from 'react-native-youtube-bridge'; function App() { return ( - + ) } ``` @@ -120,7 +124,7 @@ function App() { @@ -158,7 +162,7 @@ YouTube 내장 플레이어의 [매개변수](https://developers.google.com/yout function App() { return ( diff --git a/README.md b/README.md index e08e932..456cfaf 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,11 @@ import { YoutubePlayer } from 'react-native-youtube-bridge'; function App() { return ( - + ) } ``` @@ -120,7 +124,7 @@ function App() { @@ -158,7 +162,7 @@ You can customize the playback environment by configuring YouTube embedded playe function App() { return ( diff --git a/example/src/App.tsx b/example/src/App.tsx index 3433773..9d922b0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -174,7 +174,7 @@ function App() { ( ( { - videoId, + source, width = screenWidth, height = 200, progressInterval, @@ -40,6 +41,8 @@ const YoutubePlayer = forwardRef( ) => { const { startTime = 0, endTime } = playerVars; + const videoId = useYouTubeVideoId(source); + const webViewRef = useRef(null); const [isReady, setIsReady] = useState(false); const commandIdRef = useRef(0); diff --git a/src/YoutubePlayer.web.tsx b/src/YoutubePlayer.web.tsx index 5ffdf49..4952281 100644 --- a/src/YoutubePlayer.web.tsx +++ b/src/YoutubePlayer.web.tsx @@ -2,10 +2,12 @@ import { forwardRef, useImperativeHandle } from 'react'; import { useWindowDimensions } from 'react-native'; import YoutubePlayerWrapper from './YoutubePlayerWrapper'; import useYouTubePlayer from './hooks/useYoutubePlayer'; +import useYouTubeVideoId from './hooks/useYoutubeVideoId'; import type { PlayerControls, YoutubePlayerProps } from './types/youtube'; const YoutubePlayer = forwardRef((props, ref) => { - const { containerRef, controls } = useYouTubePlayer(props); + const videoId = useYouTubeVideoId(props.source); + const { containerRef, controls } = useYouTubePlayer({ ...props, videoId }); const { width: screenWidth } = useWindowDimensions(); useImperativeHandle(ref, () => controls, [controls]); diff --git a/src/hooks/useCreateLocalPlayerHtml.ts b/src/hooks/useCreateLocalPlayerHtml.ts index 0510df0..9762a0f 100644 --- a/src/hooks/useCreateLocalPlayerHtml.ts +++ b/src/hooks/useCreateLocalPlayerHtml.ts @@ -17,7 +17,6 @@ const useCreateLocalPlayerHtml = ({ }: YoutubePlayerVars & { videoId: string }) => { const createPlayerHTML = useCallback(() => { if (!validateVideoId(videoId)) { - console.error('Invalid YouTube video ID:', videoId); return '
Invalid video ID
'; } diff --git a/src/hooks/useYoutubeVideoId.ts b/src/hooks/useYoutubeVideoId.ts new file mode 100644 index 0000000..b4fc59c --- /dev/null +++ b/src/hooks/useYoutubeVideoId.ts @@ -0,0 +1,56 @@ +import { useMemo } from 'react'; +import { ERROR_CODES, type PlayerEvents, type YouTubeSource } from '../types/youtube'; +import { extractVideoIdFromUrl, validateVideoId } from '../utils/validate'; + +const useYouTubeVideoId = (source: YouTubeSource, onError?: PlayerEvents['onError']): string => { + // biome-ignore lint/correctness/useExhaustiveDependencies: + const sourceValue = useMemo(() => { + if (typeof source === 'string') { + return source; + } + + if ('videoId' in source) { + return source.videoId; + } + + if ('url' in source) { + return source.url; + } + + return null; + }, [ + typeof source === 'string' ? source : 'videoId' in source ? source.videoId : 'url' in source ? source.url : null, + ]); + + const videoId = useMemo(() => { + if (!sourceValue) { + console.error('Invalid YouTube source: ', sourceValue); + onError?.({ + code: 1002, + message: ERROR_CODES[1002], + }); + return ''; + } + + if (validateVideoId(sourceValue)) { + return sourceValue; + } + + const extractedId = extractVideoIdFromUrl(sourceValue); + + if (!extractedId) { + console.error('Invalid YouTube source: ', sourceValue); + onError?.({ + code: 1002, + message: ERROR_CODES[1002], + }); + return ''; + } + + return extractedId; + }, [sourceValue, onError]); + + return videoId; +}; + +export default useYouTubeVideoId; diff --git a/src/modules/YouTubePlayerCore.tsx b/src/modules/YouTubePlayerCore.tsx index dc60507..f82c45b 100644 --- a/src/modules/YouTubePlayerCore.tsx +++ b/src/modules/YouTubePlayerCore.tsx @@ -1,7 +1,11 @@ import type { YouTubePlayer } from '../types/iframe'; -import { ERROR_CODES, type YoutubePlayerProps as PlayerConfig, type PlayerEvents, PlayerState } from '../types/youtube'; +import { ERROR_CODES, type PlayerEvents, PlayerState, type YoutubePlayerProps } from '../types/youtube'; import { validateVideoId } from '../utils/validate'; +type PlayerConfig = Omit & { + videoId: string; +}; + class YouTubePlayerCore { private player: YouTubePlayer | null = null; private progressInterval: NodeJS.Timeout | null = null; diff --git a/src/types/youtube.ts b/src/types/youtube.ts index 4861a57..c7542e5 100644 --- a/src/types/youtube.ts +++ b/src/types/youtube.ts @@ -51,9 +51,10 @@ export type PlayerEvents = { onAutoplayBlocked?: () => void; }; -// YouTube IFrame API official documentation based +export type YouTubeSource = string | { videoId: string } | { url: string }; + export type YoutubePlayerProps = { - videoId: string; + source: YouTubeSource; width?: DimensionValue; height?: DimensionValue; /** diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 9aa83f4..7f2da47 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -1,23 +1,19 @@ -export const validateVideoId = (videoId?: string): boolean => { - // YouTube video ID is 11 characters of alphanumeric and hyphen, underscore - const videoIdRegex = /^[a-zA-Z0-9_-]{11}$/; - return videoIdRegex.test(videoId ?? ''); -}; +const MATCH_URL_YOUTUBE = + /(?:youtu\.be\/|youtube(?:-nocookie|education)?\.com\/(?:embed\/|v\/|watch\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))((\w|-){11})/; -export const extractVideoIdFromUrl = (url: string): string | null => { - const patterns = [ - /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, - /youtube\.com\/v\/([^&\n?#]+)/, - ]; - - for (const pattern of patterns) { - const match = url.match(pattern); - if (match?.[1]) { - return match[1]; - } +export const extractVideoIdFromUrl = (url?: string): string | undefined => { + if (!url) { + return undefined; } - return null; + const match = url.match(MATCH_URL_YOUTUBE); + + return match ? match[1] : undefined; +}; + +export const validateVideoId = (videoId?: string): boolean => { + const videoIdRegex = /^[\w-]{11}$/; + return videoIdRegex.test(videoId ?? ''); }; export const escapeHtml = (unsafe?: string): string => {