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
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": ["example"]
}
25 changes: 25 additions & 0 deletions .changeset/funny-moons-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"react-native-youtube-bridge": major
"@react-native-youtube-bridge/react": major
"@react-native-youtube-bridge/core": major
"@react-native-youtube-bridge/web": major
---

feat!: introduce hooks-based API for v2.0

BREAKING CHANGE: Complete API redesign with React hooks

- Replace `YoutubePlayer` component with `YoutubeView` + `useYouTubePlayer` hook
- Add `useYouTubeEvent` hook for reactive event handling
- Remove ref-based imperative API in favor of declarative approach
- Simplify component props and reduce coupling between components
- Follow expo patterns for better DX

Migration required from v1:

- `YoutubePlayer` → `YoutubeView` + `useYouTubePlayer()`
- Event props → `useYouTubeEvent()` hooks
- `playerRef.current.method()` → `player.method()`

Fixes: Memory leaks, complex state management, tight coupling
Improves: Developer experience, maintainability, performance
47 changes: 0 additions & 47 deletions example/CHANGELOG.md

This file was deleted.

216 changes: 96 additions & 120 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Alert, Platform, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import {
type PlayerControls,
type PlayerInfo,
PlayerState,
type ProgressData,
type YouTubeError,
YoutubePlayer,
YoutubeView,
useYouTubeEvent,
useYouTubePlayer,
useYoutubeOEmbed,
} from 'react-native-youtube-bridge';

Expand All @@ -17,22 +15,89 @@ const formatTime = (seconds: number): string => {
};

function App() {
const playerRef = useRef<PlayerControls>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [loadedFraction, setLoadedFraction] = useState(0);
const [playbackRate, setPlaybackRate] = useState(1);
const [availableRates, setAvailableRates] = useState<number[]>([1]);
const [volume, setVolume] = useState(100);
const [isMuted, setIsMuted] = useState(false);
const [videoId, setVideoId] = useState('AbZH7XWDW_k');
const [progressInterval, setProgressInterval] = useState(1000);
const { oEmbed, isLoading, error } = useYoutubeOEmbed(`https://www.youtube.com/watch?v=${videoId}`);

console.log('oEmbed', oEmbed, isLoading, error);
const player = useYouTubePlayer(videoId, {
autoplay: true,
controls: true,
playsinline: true,
rel: false,
muted: true,
});

const handleReady = useCallback((playerInfo: PlayerInfo) => {
const changePlaybackRate = (rate: number) => {
player.setPlaybackRate(rate);
};

const changeVolume = (newVolume: number) => {
player.setVolume(newVolume);
setVolume(newVolume);
};

const toggleMute = useCallback(() => {
if (isMuted) {
player.unMute();
setIsMuted(false);
return;
}

player.mute();
setIsMuted(true);
}, [player, isMuted]);

const onPlay = useCallback(() => {
if (isPlaying) {
player.pause();
return;
}

player.play();
}, [player, isPlaying]);

const getPlayerInfo = async () => {
const [currentTime, duration, url, state, loaded] = await Promise.all([
player.getCurrentTime(),
player.getDuration(),
player.getVideoUrl(),
player.getPlayerState(),
player.getVideoLoadedFraction(),
]);

console.log(
`
currentTime: ${currentTime}
duration: ${duration}
url: ${url}
state: ${state}
loaded: ${loaded}
`,
);

Alert.alert(
'Player info',
`Current time: ${formatTime(currentTime || 0)}\n` +
`duration: ${formatTime(duration || 0)}\n` +
`state: ${state}\n` +
`loaded: ${((loaded || 0) * 100).toFixed(1)}%\n` +
`url: ${url || 'N/A'}`,
);
};

const playbackRate = useYouTubeEvent(player, 'playbackRateChange', 1);
const playbackQuality = useYouTubeEvent(player, 'playbackQualityChange');
const progress = useYouTubeEvent(player, 'progress', progressInterval);

const currentTime = progress?.currentTime ?? 0;
const duration = progress?.duration ?? 0;
const loadedFraction = progress?.loadedFraction ?? 0;

useYouTubeEvent(player, 'ready', (playerInfo) => {
console.log('Player is ready!');
Alert.alert('Alert', 'YouTube player is ready!');

Expand All @@ -51,9 +116,9 @@ function App() {
if (playerInfo?.muted !== undefined) {
setIsMuted(playerInfo.muted);
}
}, []);
});

const handleStateChange = useCallback((state: PlayerState) => {
useYouTubeEvent(player, 'stateChange', (state) => {
console.log('Player state changed:', state);
setIsPlaying(state === PlayerState.PLAYING);

Expand All @@ -77,93 +142,21 @@ function App() {
console.log('Video is cued');
break;
}
}, []);
});

const handleProgress = useCallback((progress: ProgressData) => {
setCurrentTime(progress.currentTime);
setDuration(progress.duration);
setLoadedFraction(progress.loadedFraction);
}, []);
useYouTubeEvent(player, 'autoplayBlocked', () => {
console.log('Autoplay was blocked');
});

const handleError = useCallback((error: YouTubeError) => {
useYouTubeEvent(player, 'error', (error) => {
console.error('Player error:', error);
Alert.alert('Error', `Player error (${error.code}): ${error.message}`);
}, []);
});

const handlePlaybackRateChange = useCallback((rate: number) => {
console.log('Playback rate changed:', rate);
setPlaybackRate(rate);
}, []);

const handlePlaybackQualityChange = useCallback((quality: string) => {
console.log('Playback quality changed:', quality);
}, []);

const handleAutoplayBlocked = useCallback(() => {
console.log('Autoplay was blocked');
}, []);

const changePlaybackRate = (rate: number) => {
playerRef.current?.setPlaybackRate(rate);
};

const changeVolume = (newVolume: number) => {
playerRef.current?.setVolume(newVolume);
setVolume(newVolume);
};

const toggleMute = useCallback(() => {
if (isMuted) {
playerRef.current?.unMute();
setIsMuted(false);
return;
}

playerRef.current?.mute();
setIsMuted(true);
}, [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([
playerRef.current?.getCurrentTime(),
playerRef.current?.getDuration(),
playerRef.current?.getVideoUrl(),
playerRef.current?.getPlayerState(),
playerRef.current?.getVideoLoadedFraction(),
]);

console.log(
`
currentTime: ${currentTime}
duration: ${duration}
url: ${url}
state: ${state}
loaded: ${loaded}
`,
);

Alert.alert(
'Player info',
`Current time: ${formatTime(currentTime || 0)}\n` +
`duration: ${formatTime(duration || 0)}\n` +
`state: ${state}\n` +
`loaded: ${((loaded || 0) * 100).toFixed(1)}%\n` +
`url: ${url || 'N/A'}`,
);
} catch (error) {
console.error('Error getting player info:', error);
}
};
useEffect(() => {
console.log('oEmbed', oEmbed, isLoading, error);
console.log('playbackQuality', playbackQuality);
}, [oEmbed, isLoading, error, playbackQuality]);

return (
<SafeAreaView style={styles.container}>
Expand All @@ -173,30 +166,13 @@ function App() {
<Text style={styles.subtitle}>Video ID: {videoId}</Text>
<Text style={styles.subtitle}>Playback rate: {playbackRate}x</Text>
</View>

<YoutubePlayer
ref={playerRef}
source={videoId}
<YoutubeView
useInlineHtml
player={player}
height={Platform.OS === 'web' ? 'auto' : undefined}
useInlineHtml={false}
playerVars={{
autoplay: true,
controls: true,
playsinline: true,
rel: false,
muted: true,
}}
webViewProps={{
renderToHardwareTextureAndroid: true,
}}
progressInterval={progressInterval}
onReady={handleReady}
onStateChange={handleStateChange}
onProgress={handleProgress}
onError={handleError}
onPlaybackRateChange={handlePlaybackRateChange}
onPlaybackQualityChange={handlePlaybackQualityChange}
onAutoplayBlocked={handleAutoplayBlocked}
style={{
maxHeight: 400,
}}
Expand Down Expand Up @@ -235,7 +211,7 @@ function App() {
<View style={styles.controls}>
<TouchableOpacity
style={[styles.button, styles.seekButton]}
onPress={() => playerRef.current?.seekTo(currentTime > 10 ? currentTime - 10 : 0)}
onPress={() => player.seekTo(currentTime > 10 ? currentTime - 10 : 0)}
>
<Text style={styles.buttonText}>⏪ -10s</Text>
</TouchableOpacity>
Expand All @@ -244,13 +220,13 @@ function App() {
<Text style={styles.buttonText}>{isPlaying ? '⏸️ Pause' : '▶️ Play'}</Text>
</TouchableOpacity>

<TouchableOpacity style={[styles.button, styles.stopButton]} onPress={() => playerRef.current?.stop()}>
<TouchableOpacity style={[styles.button, styles.stopButton]} onPress={() => player.stop()}>
<Text style={styles.buttonText}>⏹️ Stop</Text>
</TouchableOpacity>

<TouchableOpacity
style={[styles.button, styles.seekButton]}
onPress={() => playerRef.current?.seekTo(currentTime + 10, true)}
onPress={() => player.seekTo(currentTime + 10, true)}
>
<Text style={styles.buttonText}>⏭️ +10s</Text>
</TouchableOpacity>
Expand Down
Loading