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)
I ended up fixing the issue by taking { Pressable }
or { TouchableOpacity }
from react-native
instead of react-native-gesture-handler