androidreactjsreact-nativereact-native-video

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


I have a React Native project ver .66.4 with React-native-video 5.2.0 and React-native-video-controls 2.8.1

I have a VideoPlayer component that has custom controls built into the ref. This component works perfectly in iOS, but it doesn't work on Android. The controls don't update when pressed (play doesn't turn into pause) and in fullscreen it seems there's a view or something blocking the buttons.

My VideoPlayer component:

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)

Solution

  • I ended up fixing the issue by taking { Pressable } or { TouchableOpacity } from react-native instead of react-native-gesture-handler