You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 03:25:11