wonday / react-native-orientation-locker

A react-native module that can listen on orientation changing of device, get current orientation, lock to preferred orientation.
MIT License
758 stars 273 forks source link

.lockToPortrait crashes iOS - works fine on Android. #215

Open jrhager84 opened 2 years ago

jrhager84 commented 2 years ago

Not sure why or how, but the logic to lock to portrait doesn't work on iOS. Here is the code. The _app.tsx file just has a blank dependency useEffect that locks it to portrait (I only want specific screens during certain state changes to have landscape mode).

Here is my 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.blue4 : "#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}
            ignoreSilentSwitch='ignore'
            muted={videoRef.current?.state.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(() => {
    if (error) console.log("ERROR", error)
  }, [error])

  useEffect(() => {
    const unsubscribe = navigation.addListener('blur', () => {
      if (videoRef.current.state.paused == false) videoRef.current.methods.togglePlayPause();
    });

    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)

I can do pretty much any other lock except portrait.

kiranNegiloni commented 1 year ago

Hi @jrhager84, I am facing similar issue for IOS. App is getting crashed on invoking lockToPortrait (Only on physical device, On Simulator its working fine). Any help would be highly appreciated. Thanks.

lukerlv commented 4 months ago

It may be the "supportedOrientations={['landscape']}" cause?