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
来解决问题
我有一个 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