Skip to content

Commit b4cdf79

Browse files
authored
fix: where onProgress is not called when seekTo is invoked (#15)
1 parent df8960e commit b4cdf79

File tree

6 files changed

+163
-17
lines changed

6 files changed

+163
-17
lines changed

.changeset/gentle-houses-attack.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"react-native-youtube-bridge": patch
3+
---
4+
5+
fix: where onProgress is not called when seekTo is invoked
6+
- add TSDoc documentation
7+
- add defensive logic for cases without videoId
8+
- fix issue where seekTo doesn't work properly when paused without interval

src/YoutubePlayer.web.tsx

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useWindowDimensions } from 'react-native';
33
import YoutubePlayerWrapper from './YoutubePlayerWrapper';
44
import type { YouTubePlayer } from './types/iframe';
55
import { ERROR_CODES, type PlayerControls, PlayerState, type YoutubePlayerProps } from './types/youtube';
6+
import { validateVideoId } from './utils/validate';
67

78
const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
89
(
@@ -40,6 +41,7 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
4041
const createPlayerRef = useRef<() => void>(null);
4142
const progressInterval = useRef<NodeJS.Timeout | null>(null);
4243
const intervalRef = useRef<number>(interval);
44+
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
4345

4446
const stopProgressTracking = useCallback(() => {
4547
if (progressInterval.current) {
@@ -48,6 +50,24 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
4850
}
4951
}, []);
5052

53+
const sendProgress = useCallback(async () => {
54+
if (!playerRef.current || !playerRef.current.getCurrentTime) {
55+
return;
56+
}
57+
58+
const currentTime = await playerRef.current.getCurrentTime();
59+
const duration = await playerRef.current.getDuration();
60+
const percentage = duration > 0 ? (currentTime / duration) * 100 : 0;
61+
const loadedFraction = await playerRef.current.getVideoLoadedFraction();
62+
63+
onProgress?.({
64+
currentTime,
65+
duration,
66+
percentage,
67+
loadedFraction,
68+
});
69+
}, [onProgress]);
70+
5171
const startProgressTracking = useCallback(() => {
5272
if (!intervalRef.current) {
5373
return;
@@ -120,7 +140,12 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
120140
}, []);
121141

122142
createPlayerRef.current = () => {
123-
if (!containerRef.current || !window.YT?.Player || !videoId) {
143+
if (!containerRef.current || !window.YT?.Player) {
144+
return;
145+
}
146+
147+
if (!validateVideoId(videoId)) {
148+
onError?.({ code: -2, message: 'Invalid YouTube videoId supplied' });
124149
return;
125150
}
126151

@@ -171,10 +196,35 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
171196
const state = event.data;
172197
console.log('YouTube player state changed:', state);
173198
onStateChange?.(state);
199+
200+
if (state === PlayerState.ENDED) {
201+
stopProgressTracking();
202+
sendProgress();
203+
return;
204+
}
205+
174206
if (state === PlayerState.PLAYING) {
175207
startProgressTracking();
176208
return;
177209
}
210+
211+
if (state === PlayerState.PAUSED) {
212+
stopProgressTracking();
213+
sendProgress();
214+
return;
215+
}
216+
217+
if (state === PlayerState.BUFFERING) {
218+
startProgressTracking();
219+
return;
220+
}
221+
222+
if (state === PlayerState.CUED) {
223+
stopProgressTracking();
224+
sendProgress();
225+
return;
226+
}
227+
178228
stopProgressTracking();
179229
},
180230
onError: (event) => {
@@ -225,7 +275,7 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
225275
}, [createPlayer, stopProgressTracking]);
226276

227277
useEffect(() => {
228-
if (playerRef.current && videoId) {
278+
if (playerRef.current && validateVideoId(videoId)) {
229279
try {
230280
playerRef.current.loadVideoById(videoId);
231281
} catch (error) {
@@ -258,9 +308,20 @@ const YoutubePlayer = forwardRef<PlayerControls, YoutubePlayerProps>(
258308
playerRef.current?.stopVideo();
259309
}, []);
260310

261-
const seekTo = useCallback((seconds: number, allowSeekAhead = true) => {
262-
playerRef.current?.seekTo(seconds, allowSeekAhead);
263-
}, []);
311+
const seekTo = useCallback(
312+
(seconds: number, allowSeekAhead = true) => {
313+
playerRef.current?.seekTo(seconds, allowSeekAhead);
314+
315+
if (seekTimeoutRef.current) {
316+
clearTimeout(seekTimeoutRef.current);
317+
}
318+
319+
seekTimeoutRef.current = setTimeout(() => {
320+
sendProgress();
321+
}, 200);
322+
},
323+
[sendProgress],
324+
);
264325

265326
const setVolume = useCallback((volume: number) => {
266327
playerRef.current?.setVolume(volume);

src/hooks/useCreateLocalPlayerHtml.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const useCreateLocalPlayerHtml = ({
7373
var isDestroyed = false;
7474
7575
${youtubeIframeScripts.receiveMessage}
76+
${youtubeIframeScripts.sendProgress}
7677
7778
function cleanup() {
7879
isDestroyed = true;
@@ -133,7 +134,17 @@ const useCreateLocalPlayerHtml = ({
133134
play: () => player && player.playVideo(),
134135
pause: () => player && player.pauseVideo(),
135136
stop: () => player && player.stopVideo(),
136-
seekTo: (seconds, allowSeekAhead) => player && player.seekTo(seconds, allowSeekAhead !== false),
137+
seekTo: (seconds, allowSeekAhead) => {
138+
if (!player) {
139+
return;
140+
}
141+
142+
player.seekTo(seconds, allowSeekAhead !== false);
143+
144+
setTimeout(() => {
145+
sendProgress();
146+
}, 200);
147+
},
137148
138149
setVolume: (volume) => player && player.setVolume(volume),
139150
getVolume: () => player ? player.getVolume() : 0,

src/hooks/youtubeIframeScripts.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ const stopProgressTracking = /* js */ `
4444
}
4545
`;
4646

47+
const sendProgress = /* js */ `
48+
function sendProgress() {
49+
if (player && player.getCurrentTime) {
50+
try {
51+
const currentTime = player.getCurrentTime();
52+
const duration = player.getDuration();
53+
const percentage = duration > 0 ? (currentTime / duration) * 100 : 0;
54+
const loadedFraction = player.getVideoLoadedFraction();
55+
56+
window.ReactNativeWebView.postMessage(JSON.stringify({
57+
type: 'progress',
58+
currentTime,
59+
duration,
60+
percentage,
61+
loadedFraction,
62+
}));
63+
} catch (error) {
64+
console.error('Final progress error:', error);
65+
}
66+
}
67+
}
68+
`;
69+
4770
const onPlayerReady = /* js */ `
4871
function onPlayerReady(event) {
4972
if (isDestroyed) {
@@ -62,7 +85,7 @@ const onPlayerReady = /* js */ `
6285
}
6386
`;
6487

65-
const onPlayerStateChange = /* js */ `
88+
const onPlayerStateChange = /* js */ `
6689
function onPlayerStateChange(event) {
6790
if (isDestroyed) {
6891
return;
@@ -74,11 +97,35 @@ const onPlayerStateChange = /* js */ `
7497
state: event.data
7598
}));
7699
100+
if (event.data === YT.PlayerState.ENDED) {
101+
stopProgressTracking();
102+
sendProgress();
103+
return;
104+
}
105+
77106
if (event.data === YT.PlayerState.PLAYING) {
78107
startProgressTracking();
79-
} else {
108+
return;
109+
}
110+
111+
if (event.data === YT.PlayerState.PAUSED) {
112+
stopProgressTracking();
113+
sendProgress();
114+
return;
115+
}
116+
117+
if (event.data === YT.PlayerState.BUFFERING) {
118+
startProgressTracking();
119+
return;
120+
}
121+
122+
if (event.data === YT.PlayerState.CUED) {
80123
stopProgressTracking();
124+
sendProgress();
125+
return;
81126
}
127+
128+
stopProgressTracking();
82129
} catch (error) {
83130
console.error('onPlayerStateChange error:', error);
84131
}
@@ -229,6 +276,7 @@ export const youtubeIframeScripts = {
229276
startProgressTracking,
230277
stopProgressTracking,
231278
receiveMessage,
279+
sendProgress,
232280
onPlayerReady,
233281
onPlayerStateChange,
234282
onPlayerError,

src/types/youtube.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,33 @@ import type { WebViewProps } from 'react-native-webview';
44

55
export type YoutubePlayerVars = {
66
/**
7-
* @description If the `muted` is not set to true when activating the `autoplay`, it may not work properly depending on browser policy. (https://developer.chrome.com/blog/autoplay)
7+
* @description If the `muted` is not set to true when activating the `autoplay`,
8+
* it may not work properly depending on browser policy. (https://developer.chrome.com/blog/autoplay)
89
*/
910
autoplay?: boolean;
11+
/**
12+
* @description If the `controls` is set to true, the player will display the controls.
13+
*/
1014
controls?: boolean;
15+
/**
16+
* @description If the `loop` is set to true, the player will loop the video.
17+
*/
1118
loop?: boolean;
19+
/**
20+
* @description If the `muted` is set to true, the player will be muted.
21+
*/
1222
muted?: boolean;
1323
startTime?: number;
1424
endTime?: number;
1525
playsinline?: boolean;
16-
rel?: boolean; // 관련 동영상 표시
17-
origin?: string; // 보안을 위한 origin 설정
26+
/**
27+
* @description If the `rel` is set to true, the related videos will be displayed.
28+
*/
29+
rel?: boolean;
30+
/**
31+
* @description The origin of the player.
32+
*/
33+
origin?: string;
1834
};
1935

2036
// YouTube IFrame API official documentation based
@@ -24,8 +40,6 @@ export type YoutubePlayerProps = {
2440
height?: DimensionValue;
2541
/**
2642
* @description The interval (in milliseconds) at which `onProgress` callback is called.
27-
* Must be a positive number to enable progress tracking.
28-
* If not provided or set to 0/falsy value, progress tracking is disabled.
2943
*/
3044
progressInterval?: number;
3145
style?: StyleProp<ViewStyle>;
@@ -43,12 +57,16 @@ export type YoutubePlayerProps = {
4357
iframeStyle?: CSSProperties;
4458

4559
// Events
60+
/**
61+
* @description Callback function called when the player is ready.
62+
*/
4663
onReady?: (playerInfo: PlayerInfo) => void;
4764
onStateChange?: (state: PlayerState) => void;
4865
onError?: (error: YouTubeError) => void;
4966
/**
50-
* @description Callback function called at the specified `progressInterval`.
51-
* Only invoked when `progressInterval` is provided as a positive number.
67+
* @description Callback function called at the specified `progressInterval`
68+
* or when `seekTo` is invoked. Only triggered when `progressInterval` is
69+
* provided as a positive number.
5270
*/
5371
onProgress?: (progress: ProgressData) => void;
5472
onPlaybackRateChange?: (playbackRate: number) => void;

src/utils/validate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
export const validateVideoId = (videoId: string): boolean => {
1+
export const validateVideoId = (videoId?: string): boolean => {
22
// YouTube video ID is 11 characters of alphanumeric and hyphen, underscore
33
const videoIdRegex = /^[a-zA-Z0-9_-]{11}$/;
4-
return videoIdRegex.test(videoId);
4+
return videoIdRegex.test(videoId ?? '');
55
};
66

77
export const extractVideoIdFromUrl = (url: string): string | null => {

0 commit comments

Comments
 (0)