React-Native-Video 控件在 iOS 中工作但在 android 中不工作? (滞后状态?)

React-Native-Video controls working in iOS but not android? (Laggy state?)

我有一个 React Native 项目版本 .66.4,带有 React-native-video 5.2.0 和 React-native-video-controls 2.8.1

我有一个 VideoPlayer 组件,它在 ref 中内置了自定义控件。此组件在 iOS 中完美,但在 Android 中不起作用。按下时控件不会更新(播放不会变成暂停)并且在全屏模式下似乎有一个视图或某些东西挡住了按钮。

我的 VideoPlayer 组件:

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { VideoProperties } from 'react-native-video';
import Video from 'react-native-video-controls';
import { Animated, DeviceEventEmitter, Dimensions, Modal, StyleSheet, Text, View } from 'react-native';
import { Image } from 'react-native-elements';
import { colors } from '../styles/colorPalette';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { useTheme } from '../contexts/ThemeContext';
import { ReactNativeProps } from 'react-native-render-html';
import { useFocusEffect, useIsFocused } from '@react-navigation/native';
import Orientation from 'react-native-orientation-locker';
interface VideoPlayerProps extends VideoProperties {
  autoPlay?: boolean
  categoryOverlay?: boolean | string
  disableSeekSkip?: boolean
  ref?: any
}
const VideoPlayer = (props: VideoPlayerProps & ReactNativeProps) => {
  const [vidAspectRatio, setVidAspectRatio] = useState(16 / 9)
  const [isFullscreen, setIsFullscreen] = useState(false)
  const { darkMode, toggleNavBar } = useTheme();
  const [error, setError] = useState(null)
  const videoRef = useRef<Video>(null);
  const progress = useRef<number>(0)
  const dimensions = {
    height: Dimensions.get('screen').height,
    width: Dimensions.get('screen').width
  }

  const handleEnterFullscreen = async () => {
    setIsFullscreen(true)
    toggleNavBar(false)
  }

  const handleExitFullscreen = async () => {
    setIsFullscreen(false)
    toggleNavBar(true)
  }

  const styles = StyleSheet.create({
    container: {
      aspectRatio: vidAspectRatio ? vidAspectRatio : 1.75,
      maxHeight: isFullscreen ? dimensions.width : dimensions.height,
      alignItems: 'center',
      justifyContent: 'center',
    },
    containerFSProps: {
      resizeMode: 'contain',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    controlsImage: {
      resizeMode: 'contain',
      width: '100%',
    },
    modalContainer: {
      flexGrow: 1,
      justifyContent: 'center',
      backgroundColor: '#000',
      resizeMode: 'contain',
    },
    playIcon: {
      color: darkMode ? colors.primary.purple4 : "#fff",
      fontSize: 30,
      marginHorizontal: 30,
    },
    playIconContainer: {
      flexDirection: 'row',
      justifyContent: 'space-around',
      alignItems: 'center',
      paddingHorizontal: 15,
      paddingVertical: 7.5,
      borderRadius: 10,
    },
    video: {
      position: 'absolute',
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
    },
    videoButton: {
      height: 60,
      width: 60,
    },
    videoPlayer: {
      position: 'absolute',
      height: '100%',
      width: '100%',
    },
    videoPoster: {
      position: 'absolute',
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
      resizeMode: 'cover',
    },
    videoWrapper: {
      position: 'absolute',
      width: '100%',
      height: '100%',
    },
    volumeOverlay: {
      position: 'absolute',
      top: 0,
      right: 0,
    },
    categoryOverlay: {
      paddingHorizontal: 10,
      paddingVertical: 5,
      position: 'absolute',
      color: '#fff',
      bottom: 10,
      right: 10,
      backgroundColor: 'rgba(0,0,0, .75)',
      borderRadius: 10,
      textTransform: 'uppercase',
    },
  });
  
  const VideoPlayerElement = useCallback((props: VideoPlayerProps & ReactNativeProps) => {
    const [duration, setDuration] = useState(null);
    const [lastTouched, setLastTouched] = useState(0)
    const [isPlaying, setIsPlaying] = useState(!props.paused || false);
    const [isSeeking, setIsSeeking] = useState(false)
    const [controlsActive, setControlsActive] = useState(false);
    const { categoryOverlay, disableSeekSkip = false, source } = props;
    const isFocused = useIsFocused();

    const handleError = (e: any) => {
      console.log("ERROR: ", e)
    }

    const handleSeek = (num: number) => {
      console.log('handleSeek')
      if (!videoRef.current || videoRef.current.state.seeking === true || (Date.now() - lastTouched < 250)) {
        return
      } else {
      videoRef.current.player.ref.seek(Math.max(0, Math.min((videoRef.current.state.currentTime + num), videoRef.current.state.duration)))
      setLastTouched(Date.now())
      }
    }

    const handleLoad = (res: any) => {
      if (progress.current > 0 && !disableSeekSkip && (progress.current != res.currentTime)) {
        videoRef.current.player.ref.seek(progress.current, 300)
      }
      // set height and duration
      duration && setDuration(res.duration ?? null);
      setVidAspectRatio(res.naturalSize ? (res.naturalSize.width / res.naturalSize.height) : (16 / 9));
    }

    const handlePause = (res: any) => {
      // The logic to handle the pause/play logic
      res.playbackRate === 0 ? setIsPlaying(false) : setIsPlaying(true);
    }

    const handlePlayPausePress = () => {
      videoRef.current.state.paused ? videoRef.current.methods.togglePlayPause(true) : videoRef.current.methods.togglePlayPause(false);
    }

    const handleProgress = (event: any) => {
      progress.current = (event.currentTime);
    }

    const handleSetControlsActive = (active: boolean) => {
      setControlsActive(active)
    }

    const convertTime = (seconds: number) => {
      const secsRemaining = Math.floor(seconds % 60);
      return `${Math.floor(seconds / 60)}:${secsRemaining < 10 ? '0' + secsRemaining : secsRemaining}`
    }

    const convertTimeV2 = (secs: number) => {
      var hours   = Math.floor(secs / 3600)
      var minutes = Math.floor(secs / 60) % 60
      var seconds = Math.floor(secs % 60)

      return [hours,minutes,seconds]
          .map(v => v < 10 ? "0" + v : v)
          .filter((v,i) => v !== "00" || i > 0)
          .join(":")
    }

    return (
      <Animated.View style={[styles.container, isFullscreen ? styles.containerFSProps : styles.containerProps]}>
        <View style={styles.videoWrapper}>
          <Video
            ref={videoRef}
            source={source}
            showOnStart
            disableBack
            disableFullscreen
            disablePlayPause
            disableSeekbar={disableSeekSkip}
            disableTimer={disableSeekSkip}
            fullscreen={isFullscreen}
            ignoreSilentSwitch="ignore"
            muted={props.muted || false}
            paused={videoRef.current?.state.paused || props.paused}
            onEnd={() => { setIsPlaying(false)}}
            onEnterFullscreen={handleEnterFullscreen}
            onExitFullscreen={handleExitFullscreen}
            onLoad={handleLoad}
            onError={handleError}
            onHideControls={() => handleSetControlsActive(false)}
            onShowControls={() => handleSetControlsActive(true)}
            onPlaybackRateChange={handlePause}
            onProgress={handleProgress}
            onSeek={() => console.log('seeking')}
            seekColor="#a146b7"
            controlTimeout={3000}
            style={{flex: 1, flexGrow: 1}}
            containerStyle={{flex: 1, flexGrow: 1}}
          />
        </View>
        {categoryOverlay && progress.current == 1 && 
          <View style={styles.categoryOverlay}>
            <Text style={{color: "#fff", textTransform: 'uppercase'}}>{(typeof categoryOverlay === 'boolean') && duration ? convertTime(duration) : categoryOverlay}</Text>
          </View>
        }
        { (progress.current == 1 && !isPlaying) && <View style={styles.videoPoster}><Image style={{width: '100%', height: '100%', resizeMode: 'contain'}} source={{ uri: `https://home.test.com${props.poster}` }} /></View> }
        { (controlsActive || !isPlaying) && 
        <>
          { (controlsActive || !videoRef.current.state.paused) && 
              <TouchableOpacity containerStyle={{position: 'absolute', top: 3, right: 0, zIndex: 999}} onPress={isFullscreen ? handleExitFullscreen : handleEnterFullscreen}>
                <Image style={{ height: 50, width: 60 }} source={isFullscreen ? require('../assets/icons/Miscellaneous/Video_Controls/minimize.png') : require('../assets/icons/Miscellaneous/Video_Controls/fullscreen.png')} />
              </TouchableOpacity>
          }
          <View style={styles.playIconContainer}>
            { !disableSeekSkip && <TouchableOpacity disabled={videoRef.current.state.currentTime == 0 || videoRef.current.state.seeking} onPress={() => handleSeek(-15)}>
              <Image containerStyle={{height: 60, width: 60}} style={styles.controlsImage} source={require('../assets/icons/Miscellaneous/Video_Controls/back-15s.png')}/>
            </TouchableOpacity> }
            <TouchableOpacity onPress={handlePlayPausePress}>
              <Image containerStyle={{height: 60, width: 60}} source={!videoRef.current.state.paused ? require('../assets/icons/Miscellaneous/Video_Controls/pause-video-white.png') : require('../assets/icons/Miscellaneous/Video_Controls/play-video-white.png')}/>
            </TouchableOpacity>
            { !disableSeekSkip && <TouchableOpacity disabled={videoRef.current.state.currentTime == videoRef.current.state.duration || videoRef.current.state.seeking} onPress={() => handleSeek(15)}>
              <Image containerStyle={{height: 60, width: 60}} style={styles.controlsImage} source={require('../assets/icons/Miscellaneous/Video_Controls/skip-15s.png')}/>
            </TouchableOpacity> }
          </View> 
        </>}
      </Animated.View>
    );
  }, [isFullscreen])

  useEffect(() => {
    Orientation.lockToPortrait()
    return () => {
      toggleNavBar(true)
    }
  }, [])

  useEffect(() => {
    if (error) console.log("ERROR", error)
  }, [error])

  useEffect(() => {
    isFullscreen ? Orientation.lockToLandscape() : Orientation.lockToPortrait()
  }, [isFullscreen])
  
  return (
    isFullscreen ? 
    <Modal hardwareAccelerated animationType='fade' visible={isFullscreen} supportedOrientations={['landscape', 'portrait']}>
      <View style={[styles.modalContainer]} >
        <VideoPlayerElement {...props} />
      </View>
    </Modal> 
    : 
    <VideoPlayerElement {...props} />
  )
}

export default React.memo(VideoPlayer)

我遇到了同样的问题,您可以使用 react-native-guesture-handler 中的 TapGestureHandler 使点击正常。

我最终通过从 react-native 中获取 { Pressable }{ TouchableOpacity } 而不是 react-native-gesture-handler

来解决问题