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
22 changes: 22 additions & 0 deletions .changeset/eight-wolves-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"react-native-youtube-bridge": minor
"@react-native-youtube-bridge/react": minor
"@react-native-youtube-bridge/core": minor
"@react-native-youtube-bridge/web": minor
---

feat: support optional YouTube source for dynamic loading

- Extend YouTubeSource type to accept undefined values
- Add defensive logic for undefined source handling
- Enable async video ID loading patterns
- Maintain backward compatibility with existing usage

New usage pattern:

```tsx
type YouTubeSource = string | { videoId: string | undefined } | { url: string | undefined } | undefined;

const [videoId, setVideoId] = useState<string | undefined>();
const player = useYouTubePlayer(videoId); // Now supports undefined
```
2 changes: 1 addition & 1 deletion packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ERROR_CODES } from '../constants';
type YouTubeErrorCode = keyof typeof ERROR_CODES;
type YouTubeErrorMessage = (typeof ERROR_CODES)[YouTubeErrorCode];

export type YouTubeSource = string | { videoId: string } | { url: string };
export type YouTubeSource = string | { videoId: string | undefined } | { url: string | undefined } | undefined;

export type ProgressData = {
currentTime: number;
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ export const extractVideoIdFromUrl = (url?: string): string | undefined => {
return match ? match[1] : undefined;
};

export const validateVideoId = (videoId?: string): boolean => {
export const validateVideoId = (videoId?: string | null): boolean => {
if (!videoId) {
return false;
}

const videoIdRegex = /^[\w-]{11}$/;
return videoIdRegex.test(videoId ?? '');
return videoIdRegex.test(videoId);
};

export const escapeHtml = (unsafe?: string): string => {
Expand Down
24 changes: 15 additions & 9 deletions packages/react-native-youtube-bridge/src/YoutubeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@ function YoutubeView({
}, [player]);

const createPlayerHTML = useCreateLocalPlayerHtml({ videoId, useInlineHtml, ...playerVars });
const webViewUrl = useMemo(
() => getYoutubeWebViewUrl(videoId, useInlineHtml, playerVars, webViewBaseUrl),
[videoId, useInlineHtml, playerVars, webViewBaseUrl],
);
const webViewUrl = getYoutubeWebViewUrl(videoId, useInlineHtml, playerVars, webViewBaseUrl);

// biome-ignore lint/correctness/useExhaustiveDependencies: webViewProps.source is intentionally excluded to prevent unnecessary re-renders
const webViewSource = useMemo(() => {
if (useInlineHtml) {
return { html: createPlayerHTML(), ...(webViewBaseUrl ? { baseUrl: webViewBaseUrl } : {}) };
}

if (webViewUrl) {
return { ...(webViewProps?.source ?? {}), uri: webViewUrl };
}

return undefined;
}, [useInlineHtml, createPlayerHTML, webViewBaseUrl, webViewUrl]);

const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
Expand Down Expand Up @@ -149,11 +159,7 @@ function YoutubeView({
{...webViewProps}
ref={webViewRef}
javaScriptEnabled
source={
useInlineHtml
? { html: createPlayerHTML(), baseUrl: webViewBaseUrl }
: { ...(webViewProps?.source ?? {}), uri: webViewUrl }
}
source={webViewSource}
onMessage={handleMessage}
onError={(error) => {
console.error('WebView error:', error);
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-youtube-bridge/src/YoutubeView.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function YoutubeView({ player, height, width, style, iframeStyle }: YoutubeViewP

const videoId = player.getVideoId();

if (!videoId) {
return;
}

const containerId = `youtube-player-${videoId}`;
containerRef.current.id = containerId;
const options = player.getOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const useCreateLocalPlayerHtml = ({
playsinline,
rel,
useInlineHtml,
}: YoutubePlayerVars & { videoId: string; useInlineHtml: boolean }) => {
}: YoutubePlayerVars & { videoId: string | null | undefined; useInlineHtml: boolean }) => {
const createPlayerHTML = useCallback(() => {
if (!useInlineHtml) {
if (!useInlineHtml || videoId === undefined) {
return '';
}

if (!validateVideoId(videoId)) {
return '<html><body><div>Invalid video ID</div></body></html>';
return '<html><body><div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #fff;">Invalid YouTube ID</div></body></html>';
}

const safeOrigin = escapeHtml(origin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ class YoutubePlayer {

private progressInterval: number | null = null;

private videoId: string;
private videoId: string | null | undefined;
private options: YoutubePlayerVars;

constructor(videoId: string, options?: YoutubePlayerVars) {
constructor(videoId: string | null | undefined, options?: YoutubePlayerVars) {
this.videoId = videoId;
this.options = options ?? {};
}

getVideoId(): string {
getVideoId(): string | null | undefined {
return this.videoId;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-youtube-bridge/src/utils/youtube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import type { YoutubePlayerVars } from '@react-native-youtube-bridge/core';
import { DEFAULT_EXTERNAL_WEB_URL } from './constants';

export const getYoutubeWebViewUrl = (
videoId: string,
videoId: string | null | undefined,
useInlineHtml: boolean,
playerVars: YoutubePlayerVars,
webViewBaseUrl?: string,
) => {
if (useInlineHtml || !videoId) {
return '';
return undefined;
}

const baseUrl = webViewBaseUrl || DEFAULT_EXTERNAL_WEB_URL;
Expand Down
10 changes: 6 additions & 4 deletions packages/react/src/hooks/useYoutubeOEmbed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ type OEmbed = {
* @param url - The URL of the YouTube video.
* @returns The oEmbed data, loading state, and error.
*/
const useYoutubeOEmbed = (url: string) => {
const useYoutubeOEmbed = (url?: string) => {
const [oEmbed, setOEmbed] = useState<OEmbed>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (!url) {
return;
}

const controller = new AbortController();

setError(null);
Expand Down Expand Up @@ -61,9 +65,7 @@ const useYoutubeOEmbed = (url: string) => {
}
};

if (url) {
fetchOEmbed();
}
fetchOEmbed();

return () => {
controller.abort();
Expand Down
24 changes: 19 additions & 5 deletions packages/react/src/hooks/useYoutubeVideoId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
} from '@react-native-youtube-bridge/core';
import { useMemo } from 'react';

const useYouTubeVideoId = (source: YouTubeSource, onError?: PlayerEvents['onError']): string => {
const useYouTubeVideoId = (source: YouTubeSource, onError?: PlayerEvents['onError']): string | null | undefined => {
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
const sourceValue = useMemo(() => {
if (!source) {
return;
}

if (typeof source === 'string') {
return source;
}
Expand All @@ -24,17 +28,27 @@ const useYouTubeVideoId = (source: YouTubeSource, onError?: PlayerEvents['onErro

return null;
}, [
typeof source === 'string' ? source : 'videoId' in source ? source.videoId : 'url' in source ? source.url : null,
typeof source === 'string'
? source
: source && 'videoId' in source
? source.videoId
: source && 'url' in source
? source.url
: null,
]);

const videoId = useMemo(() => {
if (!sourceValue) {
if (sourceValue === null) {
console.error('Invalid YouTube source: ', sourceValue);
onError?.({
code: 1002,
message: ERROR_CODES[1002],
});
return '';
return null;
}

if (sourceValue === undefined) {
return undefined;
}

if (validateVideoId(sourceValue)) {
Expand All @@ -49,7 +63,7 @@ const useYouTubeVideoId = (source: YouTubeSource, onError?: PlayerEvents['onErro
code: 1002,
message: ERROR_CODES[1002],
});
return '';
return null;
}

return extractedId;
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/YoutubePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function YoutubePlayer() {
}, []);

useEffect(() => {
if (!isInitialized || !containerRef.current) {
if (!isInitialized || !containerRef.current || !youtubeVideoId) {
return;
}

Expand Down