React Native多源视频嵌入最佳实践:解决旋转播放中断问题
Expo React Native 多源视频无缝旋转播放解决方案
问题根源分析
之前使用WebView和expo-av Video组件遇到的旋转中断问题,本质是:
- Android端屏幕旋转时默认重建Activity,导致组件状态丢失
- iOS端旋转时AVSession或WebView页面会话重置
- 默认竖屏锁定下,动态旋转权限未正确配合播放状态处理
一、全局屏幕旋转配置
先在app.json中配置基础旋转支持,避免系统层面的限制:
{ "expo": { "ios": { "supportsTablet": true, "infoPlist": { "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait", "UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight" ], "UISupportedInterfaceOrientations~ipad": [ "UIInterfaceOrientationPortrait", "UIInterfaceOrientationPortraitUpsideDown", "UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight" ] } }, "android": { "configChanges": ["keyboardHidden", "orientation", "screenSize"], "screenOrientation": "portrait" } } }
- Android端的
configChanges是核心,避免旋转时Activity重建 - iOS端提前声明支持的方向,后续动态控制锁定状态
然后在播放组件中动态切换旋转权限:
import { ScreenOrientation } from 'expo'; // 播放开始时解锁旋转 const unlockRotation = async () => { await ScreenOrientation.unlockAsync(); }; // 播放结束/退出时恢复竖屏锁定 const lockRotation = async () => { await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT); };
二、分视频源适配方案
1. 自定义MP4链接:优化expo-av Video组件
通过useRef保存播放实例和进度,避免旋转时状态丢失:
import { Video } from 'expo-av'; import { useState, useRef, useEffect } from 'react'; const MP4Player = ({ src }) => { const videoRef = useRef(null); const [playbackStatus, setPlaybackStatus] = useState(null); const [lastPlayPosition, setLastPlayPosition] = useState(0); // 旋转时保存当前播放进度 useEffect(() => { const subscription = ScreenOrientation.addOrientationChangeListener(() => { if (playbackStatus?.isPlaying) { setLastPlayPosition(playbackStatus.positionMillis); } }); return () => subscription.remove(); }, [playbackStatus]); // 旋转后自动恢复播放 useEffect(() => { if (lastPlayPosition > 0 && playbackStatus?.isLoaded && !playbackStatus.isPlaying) { videoRef.current?.setPositionAsync(lastPlayPosition); videoRef.current?.playAsync(); } }, [playbackStatus, lastPlayPosition]); return ( <Video ref={videoRef} source={{ uri: src }} style={{ width: '100%', height: 220 }} useNativeControls={true} resizeMode="contain" onPlaybackStatusUpdate={status => setPlaybackStatus(status)} mediaPlaybackRequiresUserAction={true} /> ); };
2. YouTube/Vimeo/Microsoft Stream:优化WebView方案
使用嵌入链接替代直接视频页面,配合JS注入维持播放状态:
import { WebView } from 'react-native-webview'; import { useState, useRef, useEffect } from 'react'; const WebVideoPlayer = ({ src }) => { const webViewRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); // 转换为各平台嵌入链接 const getEmbedUrl = () => { if (src.includes('youtube.com') || src.includes('youtu.be')) { const videoId = src.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/)?.[1]; return videoId ? `https://www.youtube.com/embed/${videoId}?enablejsapi=1` : src; } if (src.includes('vimeo.com')) { const videoId = src.match(/vimeo\.com\/(\d+)/)?.[1]; return videoId ? `https://player.vimeo.com/video/${videoId}?api=1` : src; } if (src.includes('microsoftstream.com')) { const videoId = src.match(/video\/([^\/]+)/)?.[1]; return videoId ? `https://web.microsoftstream.com/embed/video/${videoId}` : src; } return src; }; // 注入JS监听播放状态,旋转后恢复播放 const injectedJS = ` const video = document.querySelector('video'); if (video) { video.addEventListener('play', () => window.ReactNativeWebView.postMessage('play')); video.addEventListener('pause', () => window.ReactNativeWebView.postMessage('pause')); document.addEventListener('visibilitychange', () => { if (!document.hidden && ${isPlaying}) video.play(); }); } true; `; // 旋转时主动触发视频播放 useEffect(() => { const subscription = ScreenOrientation.addOrientationChangeListener(() => { if (isPlaying) { webViewRef.current?.injectJavaScript(`document.querySelector('video')?.play()`); } }); return () => subscription.remove(); }, [isPlaying]); return ( <WebView ref={webViewRef} javaScriptEnabled={true} scrollEnabled={false} allowsFullscreenVideo={true} allowsInlineMediaPlayback={true} renderLoading={() => <ActivityIndicator />} renderError={() => <Text>视频加载失败</Text>} userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" source={{ uri: getEmbedUrl() }} mediaPlaybackRequiresUserAction={true} injectedJavaScript={injectedJS} onMessage={(e) => { if (e.data === 'play') { setIsPlaying(true); unlockRotation(); } if (e.data === 'pause') setIsPlaying(false); }} /> ); };
三、统一播放组件封装
把两种方案整合,根据视频源自动选择播放方式:
import { View } from 'react-native'; import MP4Player from './MP4Player'; import WebVideoPlayer from './WebVideoPlayer'; const VideoPlayer = ({ src }) => { const isMp4 = src.endsWith('.mp4'); return ( <View style={{ width: '100%', height: 220 }}> {isMp4 ? <MP4Player src={src} /> : <WebVideoPlayer src={src} />} </View> ); }; export default VideoPlayer;
四、关键注意事项
- iOS端必须开启
allowsInlineMediaPlayback,否则内嵌视频无法正常播放 - Android端
configChanges必须包含orientation和screenSize,禁止Activity重建 - 始终保持
mediaPlaybackRequiresUserAction={true},符合应用商店政策 - 播放结束后务必恢复竖屏锁定,避免影响应用其他页面
内容的提问来源于stack exchange,提问作者Christian Aichner
相关产品推荐
相关产品推荐

