Current behavior / Bug
As soon as I pause/play the video on custom video control or slide on slider the video keep stutter to 1 or 2 seconds later sometim even 3-5 seconds
Video Clip Bug
https://drive.google.com/file/d/1C9Nj7FMcgA-I-I5Zha5jdJ-IBGXYGWJL/view?usp=sharing
Reproduction steps
- click button to enter the new screen
- video autoplay as soon the new screen load (if not doobleTap or singleTap on overlay, it will play as normal)
- singleTap on video's overlay for the custom controls to appear then click pause button to pause video.
- click play button to play video again, then video stutter.
- Sometime slide on slider to seek new video's currentTime, video stutter (rarely not stutter)
- If you pause the video the slide and play again and custom video control fadeout, video will play as normal not stutter
Expected behavior
- pause / play video not stutter.
- slide on progress video and play video again not stutter
Platform
Which player are you experiencing the problem on:
- Android
React Native Setup
- react-native : 0.64.2
- gradle: 6.9
- JDK: 11
- Android Studio: Dolphin 2021.3.1
- Android SDK API: 30
- build tools: 30.0.2
- @react-native-community/slider: 4.3.1
- react-native-video: 5.2.0
- react-native-orientation-locker: 1.5.0
- react-native-vector-icons: 8.1.0
Remote Video URL
I called video URL from google cloud storage by using redux-saga. I already called dispatch function to called api from parent screen. So, in this screen I only useSelector to get URL from redux-saga. Can use any video URL with .mp4 file to substitute it.
Video sample
https://drive.google.com/uc?export=download&id=1C9Nj7FMcgA-I-I5Zha5jdJ-IBGXYGWJL
Sample Code
import React, { useRef, useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { View, StyleSheet, BackHandler, Dimensions, TouchableNativeFeedback, Text, StatusBar, Platform } from 'react-native';
import Video from 'react-native-video';
import Orientation from 'react-native-orientation-locker';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Slider from '@react-native-community/slider';
import { normalize } from 'react-native-elements';
const { width, height } = Dimensions.get("window");
Icon.loadFont();
let overlayTimer;
let Timer;
const VideoPlayerScreen = (props) => {
let lastTap = null;
const dispatch = useDispatch();
const { navigation } = props;
const [Fullscreen, setFullscreen] = useState(false);
const [paused, setpaused] = useState(false);
const [currentTime, setcurrentTime] = useState(0);
const [duration, setduration] = useState(0.1);
const [overlay, setoverlay] = useState(false);
const playerRef = useRef();
const contract = useSelector((state) => state.contract.contract);
useEffect(() => {
const backHandler = BackHandler.addEventListener(
"hardwareBackPress",
backAction
);
return () => backHandler.remove();
}, [])
const backAction = () => {
// navigation.goBack();
return true;
}
const videoUri = contract.videoURL
const FullscreenToggle = () => {
if (Fullscreen) {
Orientation.lockToPortrait();
StatusBar.setHidden(false)
navigation.setOptions({ headerShown: true });
setFullscreen(false)
} else {
Orientation.lockToLandscape();
StatusBar.setHidden(true)
navigation.setOptions({ headerShown: false });
setFullscreen(true);
}
}
const handleDoubleTap = (doubleTapCallback, singleTapCallback) => {
const now = Date.now();
const DOUBLE_PRESS_DELAY = 300;
if (lastTap && (now - lastTap) < DOUBLE_PRESS_DELAY) {
clearTimeout(Timer);
doubleTapCallback();
} else {
lastTap = now;
Timer = setTimeout(() => {
singleTapCallback();
}, DOUBLE_PRESS_DELAY);
}
}
const ShowHideOverlay = () => {
handleDoubleTap(() => {
}, () => {
setoverlay(true)
overlayTimer = setTimeout(() => setoverlay(false), 5000);
})
}
const backward = () => {
playerRef.current.seek(currentTime - 5);
clearTimeout(overlayTimer);
overlayTimer = setTimeout(() => setoverlay(false), 3000);
}
const forward = () => {
playerRef.current.seek(currentTime + 5);
clearTimeout(overlayTimer);
overlayTimer = setTimeout(() => setoverlay(false), 3000);
}
const onslide = (slide) => {
playerRef.current.seek(slide * duration);
clearTimeout(overlayTimer);
overlayTimer = setTimeout(() => setoverlay(false), 3000);
}
const getTime = (t) => {
const digit = n => n < 10 ? `0${n}` : `${n}`;
const sec = digit(Math.floor(t % 60));
const min = digit(Math.floor((t / 60) % 60));
const hr = digit(Math.floor((t / 3600) % 60));
// return hr + ':' + min + ':' + sec;
return min + ':' + sec;
}
const load = ({ duration }) => setduration(duration);
const progress = ({ currentTime }) => setcurrentTime(currentTime);
return (
<View style={styles.container}>
{Platform.OS === 'android' ?
< View style={Fullscreen ? styles.fullscreenVideo : styles.video}>
<Video
source={{ uri: videoUri }}
style={{ ...StyleSheet.absoluteFill }}
ref={playerRef}
paused={paused}
repeat={true}
onLoad={load}
onProgress={progress}
resizeMode={"contain"}
rate={1.0}
/>
<View style={styles.overlay}>
{overlay ?
<View style={{ ...styles.overlaySet, backgroundColor: '#0006', alignItems: 'center', justifyContent: 'space-around' }}>
<View style={{ width: 50, height: 50 }}>
<Icon name='replay-5' style={styles.icon} onPress={backward} />
</View>
<View style={{ width: 50, height: 50 }}>
<Icon name={paused ? 'play-arrow' : 'pause'} style={styles.icon} onPress={() => setpaused(!paused)} />
</View>
<View style={{ width: 50, height: 50 }}>
<Icon name='forward-5' style={styles.icon} onPress={forward} />
</View>
<View style={styles.sliderCont}>
<View style={{ ...styles.timer, alignItems: 'center' }}>
<View style={{ flexDirection: 'row' }}>
<Text style={{ color: 'white' }}>{getTime(currentTime)}/</Text>
<Text style={{ color: 'white' }}>{getTime(duration)}</Text>
</View>
<View style={{ margin: 5 }}>
<Icon onPress={FullscreenToggle}
name={Fullscreen ? 'fullscreen' : 'fullscreen-exit'}
style={{ fontSize: 20, color: 'white' }} />
</View>
</View>
<Slider
style={{ margin: 5 }}
maximumTrackTintColor='white'
minimumTrackTintColor='white'
thumbTintColor='white'
value={currentTime / duration}
onValueChange={onslide}
/>
</View>
</View>
:
<View style={styles.overlaySet}>
<TouchableNativeFeedback onPress={ShowHideOverlay}><View style={{ flex: 1 }} /></TouchableNativeFeedback>
</View>
}
</View>
</View>
:
<View style={styles.video}>
<Video
source={{ uri: videoUri }}
style={{ width: width, aspectRatio: width / (height - normalize(110)) }}
controls
// ref={(ref) => {
// this.player = ref;
// }}
/>
</View>
}
</View >
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black'
},
// video: { width, height: width * .6, backgroundColor: 'black', justifyContent: 'center', alignItems: 'center' },
video: { width: "100%", aspectRatio: width / (height - normalize(80)), backgroundColor: 'black', alignItems: 'center', justifyContent: 'center' },
fullscreenVideo: {
width: "100%",
aspectRatio: 2 / 1,
backgroundColor: 'black',
...StyleSheet.absoluteFill,
elevation: 1
},
overlay: {
...StyleSheet.absoluteFillObject,
},
overlaySet: {
flex: 1,
flexDirection: 'row',
},
icon: {
color: 'white',
flex: 1,
textAlign: 'center',
textAlignVertical: 'center',
fontSize: 25
},
TextStyle: {
fontSize: 20, textAlign: 'center',
marginVertical: 100, color: '#6200ee', fontWeight: 'bold'
},
sliderCont: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0
},
timer: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 5
},
});
export default VideoPlayerScreen;