Skip to content

Commit ae4cc4d

Browse files
authored
feat!: introduce hooks-based API for v2.0 (#64)
* 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-audio/expo-video 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 * refactor: move code that doesn't belong in the core package * refactor: suggested codereview * fix: remove singleton pattern to support multiple instances - Replace getInstance() with createInstance() factory method - Remove static instance property and related cleanup - Each YoutubePlayer now gets its own controller instance - Fixes WebView reference conflicts when multiple players exist * refactor: useYouTubeEvent initial state
1 parent 6432a30 commit ae4cc4d

28 files changed

+1108
-922
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": []
10+
"ignore": ["example"]
1111
}

.changeset/funny-moons-hear.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"react-native-youtube-bridge": major
3+
"@react-native-youtube-bridge/react": major
4+
"@react-native-youtube-bridge/core": major
5+
"@react-native-youtube-bridge/web": major
6+
---
7+
8+
feat!: introduce hooks-based API for v2.0
9+
10+
BREAKING CHANGE: Complete API redesign with React hooks
11+
12+
- Replace `YoutubePlayer` component with `YoutubeView` + `useYouTubePlayer` hook
13+
- Add `useYouTubeEvent` hook for reactive event handling
14+
- Remove ref-based imperative API in favor of declarative approach
15+
- Simplify component props and reduce coupling between components
16+
- Follow expo patterns for better DX
17+
18+
Migration required from v1:
19+
20+
- `YoutubePlayer``YoutubeView` + `useYouTubePlayer()`
21+
- Event props → `useYouTubeEvent()` hooks
22+
- `playerRef.current.method()``player.method()`
23+
24+
Fixes: Memory leaks, complex state management, tight coupling
25+
Improves: Developer experience, maintainability, performance

example/CHANGELOG.md

Lines changed: 0 additions & 47 deletions
This file was deleted.

example/src/App.tsx

Lines changed: 96 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { useCallback, useRef, useState } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import { Alert, Platform, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
33
import {
4-
type PlayerControls,
5-
type PlayerInfo,
64
PlayerState,
7-
type ProgressData,
8-
type YouTubeError,
9-
YoutubePlayer,
5+
YoutubeView,
6+
useYouTubeEvent,
7+
useYouTubePlayer,
108
useYoutubeOEmbed,
119
} from 'react-native-youtube-bridge';
1210

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

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

33-
console.log('oEmbed', oEmbed, isLoading, error);
26+
const player = useYouTubePlayer(videoId, {
27+
autoplay: true,
28+
controls: true,
29+
playsinline: true,
30+
rel: false,
31+
muted: true,
32+
});
3433

35-
const handleReady = useCallback((playerInfo: PlayerInfo) => {
34+
const changePlaybackRate = (rate: number) => {
35+
player.setPlaybackRate(rate);
36+
};
37+
38+
const changeVolume = (newVolume: number) => {
39+
player.setVolume(newVolume);
40+
setVolume(newVolume);
41+
};
42+
43+
const toggleMute = useCallback(() => {
44+
if (isMuted) {
45+
player.unMute();
46+
setIsMuted(false);
47+
return;
48+
}
49+
50+
player.mute();
51+
setIsMuted(true);
52+
}, [player, isMuted]);
53+
54+
const onPlay = useCallback(() => {
55+
if (isPlaying) {
56+
player.pause();
57+
return;
58+
}
59+
60+
player.play();
61+
}, [player, isPlaying]);
62+
63+
const getPlayerInfo = async () => {
64+
const [currentTime, duration, url, state, loaded] = await Promise.all([
65+
player.getCurrentTime(),
66+
player.getDuration(),
67+
player.getVideoUrl(),
68+
player.getPlayerState(),
69+
player.getVideoLoadedFraction(),
70+
]);
71+
72+
console.log(
73+
`
74+
currentTime: ${currentTime}
75+
duration: ${duration}
76+
url: ${url}
77+
state: ${state}
78+
loaded: ${loaded}
79+
`,
80+
);
81+
82+
Alert.alert(
83+
'Player info',
84+
`Current time: ${formatTime(currentTime || 0)}\n` +
85+
`duration: ${formatTime(duration || 0)}\n` +
86+
`state: ${state}\n` +
87+
`loaded: ${((loaded || 0) * 100).toFixed(1)}%\n` +
88+
`url: ${url || 'N/A'}`,
89+
);
90+
};
91+
92+
const playbackRate = useYouTubeEvent(player, 'playbackRateChange', 1);
93+
const playbackQuality = useYouTubeEvent(player, 'playbackQualityChange');
94+
const progress = useYouTubeEvent(player, 'progress', progressInterval);
95+
96+
const currentTime = progress?.currentTime ?? 0;
97+
const duration = progress?.duration ?? 0;
98+
const loadedFraction = progress?.loadedFraction ?? 0;
99+
100+
useYouTubeEvent(player, 'ready', (playerInfo) => {
36101
console.log('Player is ready!');
37102
Alert.alert('Alert', 'YouTube player is ready!');
38103

@@ -51,9 +116,9 @@ function App() {
51116
if (playerInfo?.muted !== undefined) {
52117
setIsMuted(playerInfo.muted);
53118
}
54-
}, []);
119+
});
55120

56-
const handleStateChange = useCallback((state: PlayerState) => {
121+
useYouTubeEvent(player, 'stateChange', (state) => {
57122
console.log('Player state changed:', state);
58123
setIsPlaying(state === PlayerState.PLAYING);
59124

@@ -77,93 +142,21 @@ function App() {
77142
console.log('Video is cued');
78143
break;
79144
}
80-
}, []);
145+
});
81146

82-
const handleProgress = useCallback((progress: ProgressData) => {
83-
setCurrentTime(progress.currentTime);
84-
setDuration(progress.duration);
85-
setLoadedFraction(progress.loadedFraction);
86-
}, []);
147+
useYouTubeEvent(player, 'autoplayBlocked', () => {
148+
console.log('Autoplay was blocked');
149+
});
87150

88-
const handleError = useCallback((error: YouTubeError) => {
151+
useYouTubeEvent(player, 'error', (error) => {
89152
console.error('Player error:', error);
90153
Alert.alert('Error', `Player error (${error.code}): ${error.message}`);
91-
}, []);
154+
});
92155

93-
const handlePlaybackRateChange = useCallback((rate: number) => {
94-
console.log('Playback rate changed:', rate);
95-
setPlaybackRate(rate);
96-
}, []);
97-
98-
const handlePlaybackQualityChange = useCallback((quality: string) => {
99-
console.log('Playback quality changed:', quality);
100-
}, []);
101-
102-
const handleAutoplayBlocked = useCallback(() => {
103-
console.log('Autoplay was blocked');
104-
}, []);
105-
106-
const changePlaybackRate = (rate: number) => {
107-
playerRef.current?.setPlaybackRate(rate);
108-
};
109-
110-
const changeVolume = (newVolume: number) => {
111-
playerRef.current?.setVolume(newVolume);
112-
setVolume(newVolume);
113-
};
114-
115-
const toggleMute = useCallback(() => {
116-
if (isMuted) {
117-
playerRef.current?.unMute();
118-
setIsMuted(false);
119-
return;
120-
}
121-
122-
playerRef.current?.mute();
123-
setIsMuted(true);
124-
}, [isMuted]);
125-
126-
const onPlay = useCallback(() => {
127-
if (isPlaying) {
128-
playerRef.current?.pause();
129-
return;
130-
}
131-
132-
playerRef.current?.play();
133-
}, [isPlaying]);
134-
135-
const getPlayerInfo = async () => {
136-
try {
137-
const [currentTime, duration, url, state, loaded] = await Promise.all([
138-
playerRef.current?.getCurrentTime(),
139-
playerRef.current?.getDuration(),
140-
playerRef.current?.getVideoUrl(),
141-
playerRef.current?.getPlayerState(),
142-
playerRef.current?.getVideoLoadedFraction(),
143-
]);
144-
145-
console.log(
146-
`
147-
currentTime: ${currentTime}
148-
duration: ${duration}
149-
url: ${url}
150-
state: ${state}
151-
loaded: ${loaded}
152-
`,
153-
);
154-
155-
Alert.alert(
156-
'Player info',
157-
`Current time: ${formatTime(currentTime || 0)}\n` +
158-
`duration: ${formatTime(duration || 0)}\n` +
159-
`state: ${state}\n` +
160-
`loaded: ${((loaded || 0) * 100).toFixed(1)}%\n` +
161-
`url: ${url || 'N/A'}`,
162-
);
163-
} catch (error) {
164-
console.error('Error getting player info:', error);
165-
}
166-
};
156+
useEffect(() => {
157+
console.log('oEmbed', oEmbed, isLoading, error);
158+
console.log('playbackQuality', playbackQuality);
159+
}, [oEmbed, isLoading, error, playbackQuality]);
167160

168161
return (
169162
<SafeAreaView style={styles.container}>
@@ -173,30 +166,13 @@ function App() {
173166
<Text style={styles.subtitle}>Video ID: {videoId}</Text>
174167
<Text style={styles.subtitle}>Playback rate: {playbackRate}x</Text>
175168
</View>
176-
177-
<YoutubePlayer
178-
ref={playerRef}
179-
source={videoId}
169+
<YoutubeView
170+
useInlineHtml
171+
player={player}
180172
height={Platform.OS === 'web' ? 'auto' : undefined}
181-
useInlineHtml={false}
182-
playerVars={{
183-
autoplay: true,
184-
controls: true,
185-
playsinline: true,
186-
rel: false,
187-
muted: true,
188-
}}
189173
webViewProps={{
190174
renderToHardwareTextureAndroid: true,
191175
}}
192-
progressInterval={progressInterval}
193-
onReady={handleReady}
194-
onStateChange={handleStateChange}
195-
onProgress={handleProgress}
196-
onError={handleError}
197-
onPlaybackRateChange={handlePlaybackRateChange}
198-
onPlaybackQualityChange={handlePlaybackQualityChange}
199-
onAutoplayBlocked={handleAutoplayBlocked}
200176
style={{
201177
maxHeight: 400,
202178
}}
@@ -235,7 +211,7 @@ function App() {
235211
<View style={styles.controls}>
236212
<TouchableOpacity
237213
style={[styles.button, styles.seekButton]}
238-
onPress={() => playerRef.current?.seekTo(currentTime > 10 ? currentTime - 10 : 0)}
214+
onPress={() => player.seekTo(currentTime > 10 ? currentTime - 10 : 0)}
239215
>
240216
<Text style={styles.buttonText}>⏪ -10s</Text>
241217
</TouchableOpacity>
@@ -244,13 +220,13 @@ function App() {
244220
<Text style={styles.buttonText}>{isPlaying ? '⏸️ Pause' : '▶️ Play'}</Text>
245221
</TouchableOpacity>
246222

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

251227
<TouchableOpacity
252228
style={[styles.button, styles.seekButton]}
253-
onPress={() => playerRef.current?.seekTo(currentTime + 10, true)}
229+
onPress={() => player.seekTo(currentTime + 10, true)}
254230
>
255231
<Text style={styles.buttonText}>⏭️ +10s</Text>
256232
</TouchableOpacity>

0 commit comments

Comments
 (0)