Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/three-papayas-design.md
Original file line number Diff line number Diff line change
@@ -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
14 changes: 9 additions & 5 deletions README-ko_kr.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ import { YoutubePlayer } from 'react-native-youtube-bridge';

function App() {
return (
<YoutubePlayer videoId={videoId} />
<YoutubePlayer
source={source} // youtube videoId or url
// OR source={{ videoId: 'AbZH7XWDW_k' }}
// OR source={{ url: 'https://youtube.com/watch?v=AbZH7XWDW_k' }}
/>
)
}
```
Expand Down Expand Up @@ -120,7 +124,7 @@ function App() {
<View>
<YoutubePlayer
ref={playerRef}
videoId={videoId}
source={source}
/>

<View style={styles.controls}>
Expand Down Expand Up @@ -158,7 +162,7 @@ YouTube 내장 플레이어의 [매개변수](https://developers.google.com/yout
function App() {
return (
<YoutubePlayer
videoId={videoId}
source={source}
playerVars={{
autoplay: true,
controls: true,
Expand All @@ -178,7 +182,7 @@ YouTube 플레이어의 스타일을 원하는 대로 커스터마이징할 수
function App() {
return (
<YoutubePlayer
videoId={videoId}
source={source}
height={400}
width={200}
style={{
Expand Down Expand Up @@ -217,7 +221,7 @@ function App() {

return (
<YoutubePlayer
videoId={videoId}
source={source}
progressInterval={1000}
onProgress={handleProgress}
/>
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ import { YoutubePlayer } from 'react-native-youtube-bridge';

function App() {
return (
<YoutubePlayer videoId={videoId} />
<YoutubePlayer
source={source} // youtube videoId or url
// OR source={{ videoId: 'AbZH7XWDW_k' }}
// OR source={{ url: 'https://youtube.com/watch?v=AbZH7XWDW_k' }}
/>
)
}
```
Expand Down Expand Up @@ -120,7 +124,7 @@ function App() {
<View>
<YoutubePlayer
ref={playerRef}
videoId={videoId}
source={source}
/>

<View style={styles.controls}>
Expand Down Expand Up @@ -158,7 +162,7 @@ You can customize the playback environment by configuring YouTube embedded playe
function App() {
return (
<YoutubePlayer
videoId={videoId}
source={source}
playerVars={{
autoplay: true,
controls: true,
Expand All @@ -178,7 +182,7 @@ You can customize the YouTube player's styling to match your application's desig
function App() {
return (
<YoutubePlayer
videoId={videoId}
source={source}
height={400}
width={200}
style={{
Expand Down Expand Up @@ -217,7 +221,7 @@ function App() {

return (
<YoutubePlayer
videoId={videoId}
source={source}
progressInterval={1000}
onProgress={handleProgress}
/>
Expand Down
2 changes: 1 addition & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function App() {

<YoutubePlayer
ref={playerRef}
videoId={videoId}
source={videoId}
height={Platform.OS === 'web' ? 'auto' : undefined}
playerVars={{
autoplay: true,
Expand Down
5 changes: 4 additions & 1 deletion src/YoutubePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type DataDetectorTypes, Dimensions, StyleSheet } from 'react-native';
import WebView, { type WebViewMessageEvent } from 'react-native-webview';
import YoutubePlayerWrapper from './YoutubePlayerWrapper';
import useCreateLocalPlayerHtml from './hooks/useCreateLocalPlayerHtml';
import useYouTubeVideoId from './hooks/useYoutubeVideoId';
import type { CommandType, MessageData } from './types/message';
import type { PlayerControls, YoutubePlayerProps } from './types/youtube';
import { safeNumber, validateVideoId } from './utils/validate';
Expand All @@ -12,7 +13,7 @@ const { width: screenWidth } = Dimensions.get('window');
const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
(
{
videoId,
source,
width = screenWidth,
height = 200,
progressInterval,
Expand Down Expand Up @@ -40,6 +41,8 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
) => {
const { startTime = 0, endTime } = playerVars;

const videoId = useYouTubeVideoId(source);

const webViewRef = useRef<WebView>(null);
const [isReady, setIsReady] = useState(false);
const commandIdRef = useRef(0);
Expand Down
4 changes: 3 additions & 1 deletion src/YoutubePlayer.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlayerControls, YoutubePlayerProps>((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]);
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useCreateLocalPlayerHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const useCreateLocalPlayerHtml = ({
}: YoutubePlayerVars & { videoId: string }) => {
const createPlayerHTML = useCallback(() => {
if (!validateVideoId(videoId)) {
console.error('Invalid YouTube video ID:', videoId);
return '<html><body><div>Invalid video ID</div></body></html>';
}

Expand Down
56 changes: 56 additions & 0 deletions src/hooks/useYoutubeVideoId.ts
Original file line number Diff line number Diff line change
@@ -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: <explanation>
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;
6 changes: 5 additions & 1 deletion src/modules/YouTubePlayerCore.tsx
Original file line number Diff line number Diff line change
@@ -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<YoutubePlayerProps, 'source'> & {
videoId: string;
};

class YouTubePlayerCore {
private player: YouTubePlayer | null = null;
private progressInterval: NodeJS.Timeout | null = null;
Expand Down
5 changes: 3 additions & 2 deletions src/types/youtube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
30 changes: 13 additions & 17 deletions src/utils/validate.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down