@@ -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);
163186function 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