2 years ago
#35390

Joel Hager
How to pass a ref to avoid reload (react-native-video)
I have a video that I'm using react native video for, and depending on whether or not the component is in full screen, it displays it in a modal. How do I pass the ref to avoid it 'reloading' the resource between state changes? Is there documentation somewhere that I can't seem to find?
It's the same element - just wrapped in a modal based on a prop.
An example:
export default function VideoPlayer() => {
return (
isFullscreen ? <Modal><Video /></Modal> : <Video />
)
}
I'm keeping it simple, because this is the logic I'm referring to. The video player works. I just want the video to not stop and reload when it re-renders based on the isFullscreen
prop.
If you are interested in the entire code:
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { VideoProperties } from 'react-native-video';
import Video from 'react-native-video-controls';
import { Animated, Dimensions, Modal, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Image } from 'react-native-elements';
import { colors } from '../styles/colorPalette';
import { useTheme } from '../contexts/ThemeContext';
import { ReactNativeProps } from 'react-native-render-html';
import Orientation from 'react-native-orientation-locker';
import { useNavigation } from '@react-navigation/native';
interface VideoPlayerProps extends VideoProperties {
autoPlay?: boolean
categoryOverlay?: boolean | string
disableSeekSkip?: boolean
ref?: any
}
const VideoPlayer = (props: VideoPlayerProps & ReactNativeProps) => {
const navigation = useNavigation();
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 = () => {
Orientation.lockToLandscape()
setIsFullscreen(true)
toggleNavBar(false)
}
const handleExitFullscreen = () => {
Platform.OS == 'ios' ? Orientation.unlockAllOrientations() : Orientation.lockToPortrait();
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: {
position: 'relative',
flexGrow: 1,
justifyContent: 'center',
backgroundColor: '#000',
resizeMode: 'contain',
zIndex: -1,
},
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,
zIndex: 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,
zIndex: 999,
textTransform: 'uppercase',
},
topControls: {
flexDirection: 'row',
justifyContent: 'flex-end',
position: 'absolute',
top: 0,
right: 0,
zIndex: 1,
}
});
const VideoPlayerElement = useCallback((props: VideoPlayerProps & ReactNativeProps) => {
const [duration, setDuration] = useState(null);
const [lastTouched, setLastTouched] = useState(0)
const [isPlaying, setIsPlaying] = useState(!props.paused || false);
const [isMuted, setIsMuted] = useState(props.muted || false)
const [controlsActive, setControlsActive] = useState(true);
const { categoryOverlay, disableSeekSkip = false, source } = props;
const handleError = (e: any) => {
console.log("ERROR: ", e)
}
const handleSeek = (num: number) => {
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 handleMute = () => {
if (isMuted) {
videoRef.current.state.muted = false
setIsMuted(false)
} else {
videoRef.current.state.muted = true
setIsMuted(true)
}
}
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}
disableVolume
doubleTapTime={0}
fullscreen={isFullscreen}
muted={isMuted}
paused={videoRef.current?.state.paused || props.paused}
onEnd={() => { setIsPlaying(false)}}
onEnterFullscreen={handleEnterFullscreen}
onExitFullscreen={handleExitFullscreen}
onMute={() => console.log('mute')}
onLoad={handleLoad}
onError={handleError}
onHideControls={() => handleSetControlsActive(false)}
onShowControls={() => handleSetControlsActive(true)}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onPlaybackRateChange={handlePause}
onProgress={handleProgress}
seekColor="#a146b7"
controlTimeout={3000}
style={{flex: 1, flexGrow: 1, zIndex: 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 || !isPlaying) &&
<View style={styles.topControls}>
<TouchableOpacity onPress={handleMute}>
<Image containerStyle={{height: 60, width: 60}} source={isMuted ? require('../assets/icons/Miscellaneous/Video_Controls/volume-muted.png') : require('../assets/icons/Miscellaneous/Video_Controls/volume-on.png')} />
</TouchableOpacity>
<TouchableOpacity onPress={isFullscreen ? handleExitFullscreen : handleEnterFullscreen}>
<Image containerStyle={{height: 60, width: 60}} source={isFullscreen ? require('../assets/icons/Miscellaneous/Video_Controls/minimize.png') : require('../assets/icons/Miscellaneous/Video_Controls/fullscreen.png')} />
</TouchableOpacity>
</View>
}
<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={isPlaying ? 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 () => {
Orientation.unlockAllOrientations()
toggleNavBar(true)
}
}, [])
useEffect(() => {
if (error) console.log("ERROR", error)
}, [error])
useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
videoRef.current.methods.togglePlayPause(false);
});
return unsubscribe;
}, [navigation]);
return (
isFullscreen ?
<Modal hardwareAccelerated animationType='slide' visible={isFullscreen} supportedOrientations={['landscape']}>
<View style={[styles.modalContainer]}>
<VideoPlayerElement {...props} />
</View>
</Modal>
:
<VideoPlayerElement {...props} />
)
}
export default React.memo(VideoPlayer)
reactjs
react-native
react-native-video
0 Answers
Your Answer