Skip to content

Commit 386079b

Browse files
committed
gg
1 parent 3dfab97 commit 386079b

5 files changed

Lines changed: 85 additions & 31 deletions

File tree

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,16 @@ set `type: 'youtube'` and put the YouTube **video id** in `uri`:
8484
<VideoPlayer source={{ id: 'y1', uri: 'dQw4w9WgXcQ', type: 'youtube' }} style={{ aspectRatio: 16 / 9 }} />
8585
```
8686

87-
YouTube plays in a WebView (IFrame API) with YouTube's own controls and native
88-
fullscreen/rotation; `usePlayback`, `useVideoEvents`, `play()/pause()/seek()`
89-
work the same. Native (`type: 'url'`, the default) and YouTube sources are
90-
interchangeable. Requires `react-native-webview`.
87+
YouTube plays in a WebView (IFrame API) but uses the **same built-in
88+
`VideoControls`** (YouTube's own UI is hidden) and the same fullscreen host —
89+
so it looks and behaves like a native source. `usePlayback`, `useVideoEvents`,
90+
`play()/pause()/seek()` all work the same. Native (`type: 'url'`, the default)
91+
and YouTube sources are interchangeable. Requires `react-native-webview`.
92+
93+
> YouTube can't re-parent its WebView the way the native engine re-parents its
94+
> view, so entering/exiting fullscreen re-creates the WebView (it resumes at
95+
> the current position). Cross-surface handoff (feed → detail) reloads for
96+
> YouTube; native video stays seamless.
9197
9298
**3. Open a detail screen with the same video** — because the `id` matches,
9399
the engine is untouched and playback continues from the exact frame:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-video-provider",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"description": "Singleton-engine video library for React Native (one native player, many surfaces)",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

src/components/FullscreenPlayer.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useVideoManager } from '../provider/VideoContext';
1313
import type { OrientationLock } from '../types/video';
1414
import { VideoControls } from './VideoControls';
1515
import { VideoSurface } from './VideoSurface';
16+
import { YouTubeView } from './YouTubeView';
1617

1718
type ModalOrientation =
1819
| 'portrait'
@@ -59,8 +60,7 @@ export function FullscreenPlayer() {
5960
const manager = useVideoManager();
6061
const fullscreen = usePlayback((s) => s.fullscreen);
6162
const fullscreenLock = usePlayback((s) => s.fullscreenLock);
62-
// YouTube handles its own fullscreen inside the WebView (native controls).
63-
const isYouTube = usePlayback((s) => s.currentVideo?.type === 'youtube');
63+
const currentVideo = usePlayback((s) => s.currentVideo);
6464

6565
// Android hardware back exits fullscreen (the iOS Modal handles its own).
6666
useEffect(() => {
@@ -74,18 +74,32 @@ export function FullscreenPlayer() {
7474
return () => sub.remove();
7575
}, [manager, fullscreen]);
7676

77-
if (!fullscreen || isYouTube) {
77+
if (!fullscreen) {
7878
return null;
7979
}
8080

81+
const isYouTube = currentVideo?.type === 'youtube';
82+
const media = isYouTube ? (
83+
<YouTubeView
84+
videoId={currentVideo!.uri}
85+
autoplay
86+
muted={manager.store.getState().muted}
87+
repeat={manager.store.getState().repeat}
88+
startSeconds={manager.store.getState().position}
89+
style={styles.surface}
90+
/>
91+
) : (
92+
<VideoSurface
93+
surfaceId={FULLSCREEN_SURFACE_ID}
94+
autoAttach
95+
style={styles.surface}
96+
/>
97+
);
98+
8199
const content = (
82100
<>
83101
<StatusBar hidden />
84-
<VideoSurface
85-
surfaceId={FULLSCREEN_SURFACE_ID}
86-
autoAttach
87-
style={styles.surface}
88-
/>
102+
{media}
89103
<VideoControls onClose={() => manager.exitFullscreen()} />
90104
</>
91105
);

src/components/VideoPlayer.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ export const VideoPlayer = forwardRef<VideoManager, VideoPlayerProps>(
152152
// Poster is shown only during the initial load — `loading` is true from
153153
// setSource until onLoad, and stays false for mid-stream buffering.
154154
const loading = usePlayback((s) => s.loading);
155+
// For youtube: the inline WebView hands off to the fullscreen host.
156+
const fullscreen = usePlayback((s) => s.fullscreen);
155157

156158
// Read the latest source without retriggering effects on every render
157159
// (source is usually a fresh object literal each render).
@@ -296,19 +298,27 @@ export const VideoPlayer = forwardRef<VideoManager, VideoPlayerProps>(
296298
onError,
297299
});
298300

299-
// YouTube plays in a WebView with its own player UI (controls + native
300-
// fullscreen/rotation); it can't share the native surface or overlay
301-
// controls.
301+
// YouTube plays in a WebView with our own <VideoControls> overlay
302+
// (controls: 0 in the iframe). It can't re-parent its WebView, so
303+
// fullscreen hands off to the fullscreen host: the inline view unmounts
304+
// while fullscreen (avoiding double audio), resuming at the current
305+
// position on either transition.
302306
if (source.type === 'youtube') {
303307
return (
304308
<View style={[styles.container, style]} {...rest}>
305-
<YouTubeView
306-
videoId={source.uri}
307-
autoplay={autoplay}
308-
muted={muted}
309-
repeat={repeat}
310-
style={styles.surface}
311-
/>
309+
{!fullscreen ? (
310+
<>
311+
<YouTubeView
312+
videoId={source.uri}
313+
autoplay={autoplay}
314+
muted={muted}
315+
repeat={repeat}
316+
startSeconds={manager.store.getState().position}
317+
style={styles.surface}
318+
/>
319+
{controls ? <VideoControls /> : null}
320+
</>
321+
) : null}
312322
{thumbnail && loading ? (
313323
<View style={styles.surface} pointerEvents="none">
314324
{thumbnail()}

src/components/YouTubeView.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface YouTubeViewProps extends ViewProps {
2323
autoplay?: boolean;
2424
muted?: boolean;
2525
repeat?: boolean;
26+
/** Start position in seconds (captured at mount — used to resume). */
27+
startSeconds?: number;
2628
}
2729

2830
/**
@@ -37,16 +39,20 @@ export function YouTubeView({
3739
autoplay = true,
3840
muted = false,
3941
repeat = false,
42+
startSeconds = 0,
4043
style,
4144
...rest
4245
}: YouTubeViewProps) {
4346
const manager = useVideoManager();
4447
const ref = useRef<{ injectJavaScript: (js: string) => void } | null>(null);
4548
const repeatRef = useRef(repeat);
4649
repeatRef.current = repeat;
50+
// Captured once at mount so a live position prop can't rebuild the HTML
51+
// (which would reload the WebView).
52+
const startRef = useRef(Math.floor(startSeconds));
4753

4854
const html = useMemo(
49-
() => buildHtml(videoId, autoplay, muted),
55+
() => buildHtml(videoId, autoplay, muted, startRef.current),
5056
[videoId, autoplay, muted]
5157
);
5258

@@ -133,26 +139,43 @@ export function YouTubeView({
133139
<View style={[styles.container, style]} {...rest}>
134140
<WebView
135141
ref={ref}
136-
source={{ html }}
142+
// baseUrl gives the page a real https origin — the YouTube IFrame API
143+
// rejects about:blank (null origin) with a config error.
144+
source={{ html, baseUrl: 'https://www.youtube.com' }}
137145
style={styles.web}
138146
originWhitelist={['*']}
139147
javaScriptEnabled
140148
domStorageEnabled
141149
allowsInlineMediaPlayback
142-
allowsFullscreenVideo
150+
allowsFullscreenVideo={false}
143151
mediaPlaybackRequiresUserAction={false}
152+
mixedContentMode="always"
153+
androidLayerType="hardware"
154+
setSupportMultipleWindows={false}
155+
scalesPageToFit={false}
156+
scrollEnabled={false}
157+
bounces={false}
158+
showsVerticalScrollIndicator={false}
159+
showsHorizontalScrollIndicator={false}
144160
onMessage={onMessage}
145-
// The IFrame API must load over http(s), not about:blank.
146-
baseUrl="https://www.youtube.com"
147161
/>
148162
</View>
149163
);
150164
}
151165

152-
function buildHtml(videoId: string, autoplay: boolean, muted: boolean): string {
166+
function buildHtml(
167+
videoId: string,
168+
autoplay: boolean,
169+
muted: boolean,
170+
start: number
171+
): string {
172+
// controls:0 → hide YouTube's own UI; the app draws <VideoControls>.
173+
// origin/enablejsapi are required for the IFrame API to accept commands.
153174
return `<!DOCTYPE html><html><head>
154175
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
155-
<style>html,body{margin:0;padding:0;background:#000;height:100%;overflow:hidden}#p{width:100%;height:100%}</style>
176+
<style>html,body{margin:0;padding:0;background:#000;height:100%;overflow:hidden}#p{width:100%;height:100%}
177+
/* Swallow taps on the iframe so <VideoControls> gestures win. */
178+
#p iframe{pointer-events:none}</style>
156179
</head><body>
157180
<div id="p"></div>
158181
<script>
@@ -163,7 +186,8 @@ document.body.appendChild(tag);
163186
function onYouTubeIframeAPIReady(){
164187
player=new YT.Player('p',{
165188
videoId:'${videoId}',
166-
playerVars:{autoplay:${autoplay ? 1 : 0},controls:1,playsinline:1,rel:0,modestbranding:1,fs:1,mute:${muted ? 1 : 0}},
189+
host:'https://www.youtube.com',
190+
playerVars:{autoplay:${autoplay ? 1 : 0},controls:0,playsinline:1,rel:0,modestbranding:1,fs:0,disablekb:1,iv_load_policy:3,enablejsapi:1,origin:'https://www.youtube.com',start:${start},mute:${muted ? 1 : 0}},
167191
events:{
168192
onReady:function(){post({type:'ready',duration:player.getDuration()});},
169193
onStateChange:function(e){post({type:'state',state:e.data});},

0 commit comments

Comments
 (0)