3

In my react-native project, I'm using react-navigation 5 for navigation and react-native-video for a audio/video player.

My requirement is that when a user navigates to another scren, if the audio/video should stop playing. However, that's not happening and the audio keeps playing.

I have created two screens in a stack navigator. The Video Player is a separate component.

Screen Code:

function MainScreen({ navigation }) {
  const [audiostatus, setAudioStatus] = useState(true);

  React.useEffect(() => {
    const unsubscribe = navigation.addListener('blur', () => {
      console.log('Leaving Home Screen');
      setAudioStatus(true);
    });

    return unsubscribe;
  }, [navigation]);

  return (
    <View style={{ flex: 1, justifyContent: 'center',backgroundColor: '#fff' }}>
      <Player tracks={TRACKS} paused={audiostatus} />
      <Button
        title="Go to Screen Without Audio"
        onPress={() => navigation.navigate('No Audio Screen')}
      />
      <Button
        title="Go to Screen With Another Audio (Love Yourself)"
        onPress={() => navigation.navigate('Another Audio Screen')}
      />
    </View>
  );
}

Player Code Within the Player, I recieve the paused prop to decide whether the video should be already playing or paused. Then the player has controls that control the playbck by changing the state.

export default class Player extends Component {
  constructor(props) {
    super(props);

    this.state = {
      paused: props.paused,
      totalLength: 1,
      currentPosition: 0,
      selectedTrack: 0,
      repeatOn: false,
      shuffleOn: false,
    };
  }


  setDuration(data) {
    this.setState({totalLength: Math.floor(data.duration)});
  }

  setTime(data) {
    this.setState({currentPosition: Math.floor(data.currentTime)});
  }

  seek(time) {
    time = Math.round(time);
    this.refs.audioElement && this.refs.audioElement.seek(time);
    this.setState({
      currentPosition: time,
      paused: false,
    });
  }

  render() {
    const track = this.props.tracks[this.state.selectedTrack];
    const video = this.state.isChanging ? null : (
      <Video source={{uri: track.audioUrl}} // Can be a URL or a local file.
        ref="audioElement"
        paused={this.state.paused}               // Pauses playback entirely.
        resizeMode="cover"           // Fill the whole screen at aspect ratio.
        repeat={false}                // Repeat forever.
        onLoadStart={this.loadStart} // Callback when video starts to load
        onLoad={this.setDuration.bind(this)}    // Callback when video loads
        onProgress={this.setTime.bind(this)}    // Callback every ~250ms with currentTime
        onEnd={this.onEnd}
        onError={this.videoError}
        style={styles.audioElement}
        audioOnly={true} />
    );

    return (
      <View style={styles.container}>
        <SeekBar
          onSeek={this.seek.bind(this)}
          trackLength={this.state.totalLength}
          onSlidingStart={() => this.setState({paused: true})}
          currentPosition={this.state.currentPosition} />
        <Controls
          onPressPlay={() => this.setState({paused: false})}
          onPressPause={() => this.setState({paused: true})}
          paused={this.state.paused}/>
        {video}
      </View>
    );
  }
}

The problem is that once a user starts playing the video, and then if he navigates to another screen, the video keeps playing. I want the video to pause. In the screen, i've added useEffect() to set audiostatus to pause on screen blur, but nothing happens. The video keeps playing. Please help.

asanas
  • 3,782
  • 11
  • 43
  • 72
  • Try listening for [navigation events](https://reactnavigation.org/docs/navigation-events) and pause when a screen blur or transition event is emitted. – Drew Reese Nov 09 '20 at 09:08
  • I've done that. Please see the useEffect() where I'm listening to blur event in the screen. And the listener is working as the console.log prints when I go out of screen. The prop also sets to pause true (I think). But it's not taking effect in the Player. – asanas Nov 09 '20 at 09:10
  • Ah, I see that now, sorry. Looked a little deeper and see you've a duplicate "source of truth" about the paused "state". Added answer below. – Drew Reese Nov 09 '20 at 09:24
  • I saw your answer about directly using prop instead of setting local state. I could do that. But within the player, I have some control buttons for play pause, seek, etc, And in those I change pause status with this.setState({paused: false}. I possibly can't change the prop. I wish you hadn't deleted the answer. – asanas Nov 09 '20 at 09:30
  • Yeah, I saw that your code *did actually* update that local state, so wanted to amend my answer to work with that as consuming the `paused` prop directly would be the wrong solution here. – Drew Reese Nov 09 '20 at 09:32

3 Answers3

8

A simple solution with functional components and hooks is to use

useIsFocused

which returns true or false and re-renders component when changed import it using

import { useIsFocused } from '@react-navigation/native';

 const screenIsFocused = useIsFocused();

if you're using "react-native-video" or any other library that takes something like

isPaused

you can use

 paused={isPaused || (!screenIsFocused )}

video will only run when it is not paused and the screen is also in focus

flyingPenguin
  • 141
  • 1
  • 5
3

Do the following way to pause the video

import React, {useState, useRef} from 'react';

function MainScreen({ navigation }) {
  const [audiostatus, setAudioStatus] = useState(true);

  // create ref
  const playerRef = useRef();

  React.useEffect(() => {
    const unsubscribe = navigation.addListener('blur', () => {
      console.log('Leaving Home Screen');
      setAudioStatus(false); 

      // new code add to pause video from ref
      playerRef.current.pauseVideo();
    });

    return unsubscribe;
  }, [navigation]);

  return (
    <View style={{ flex: 1, justifyContent: 'center',backgroundColor: '#fff' }}>
      <Player ... playerRef={playerRef} />
    </View>
  );
}

Convert Player class into Hooks as I did

import React, {useState, useImperativeHandle, useRef} from 'react';

function Player = (props) => {

  const [paused, setPaused] = useState(props.paused);
  const [totalLength, setTotalLength] = useState(1);
  const [currentPosition, setCurrentPosition] = useState(0);
  const [selectedTrack, setSelectedTrack] = useState(0);
  const [repeatOn, setRepeatOn] = useState(false);
  const [shuffleOn, setShuffleOn] = useState(false);
  const [isChanging, setIsChanging] = useState(false);

  const audioElement = useRef(null);

  const setDuration = (data) => {
    setTotalLength(Math.floor(data.duration));
  }

  const setTime = (data) => {
    setCurrentPosition(Math.floor(data.currentTime));
  }

  const seek = (time) => {
    time = Math.round(time);
    audioElement && audioElement.current.seek(time);
    setCurrentPosition(time);
    setPaused(false);
  }

  const loadStart = () => {}

  // add for accessing ref
  useImperativeHandle(props.playerRef, () => ({
    pauseVideo: () => setPaused(true),
  }));

    const track = props.tracks[selectedTrack];
    const video = isChanging ? null : (
      <Video source={{uri: track.audioUrl}} // Can be a URL or a local file.
        ref={audioElement}
        paused={paused}               // Pauses playback entirely.
        resizeMode="cover"
        ....
        onLoadStart={loadStart} // new added
        onLoad={setDuration} // new added
      />
    );

    return (
      <View style={styles.container}>
        <SeekBar
          onSeek={seek}
          trackLength={totalLength}
          onSlidingStart={() => setPaused(true)}
          currentPosition={currentPosition} />
        <Controls
          onPressPlay={() => setPaused(false) }
          onPressPause={() => setPaused(true)}
          paused={paused}/>
        {video}
      </View>
   );
}
Nooruddin Lakhani
  • 7,507
  • 2
  • 19
  • 39
  • I have implemented it and it's working great. Thanks. How have you utilized the setDuration function? It's not used? – asanas Nov 12 '20 at 12:02
  • actually I want to show a loading status for the time the audio loads. For that reason I had onLoadStart and onLoad in the Video component. Can I still add those? – asanas Nov 12 '20 at 12:04
  • 1
    I have implemented `onLoadStart` and `onLoad` and updated my answer so you can implement it as well – Nooruddin Lakhani Nov 12 '20 at 12:17
  • Thank you. I've awarded the bounty to you. – asanas Nov 12 '20 at 12:19
  • Just one change I made to this was to use "const Player = (props) => {" instead of "function Player = (props) => {" and then exporting it exclusively. – asanas Nov 13 '20 at 04:14
  • if I have to add two players in the same screen, then how should I use the useRef()? Should I create two instances of playerRef? as playerRef1 and playerRef2? – asanas Nov 17 '20 at 02:47
  • Yes, you would have to create two instances – Nooruddin Lakhani Nov 17 '20 at 03:55
  • Its not working anymore. I am using react-native-video 5.2.1 and it crashes saying: pauseVideo is not a function. – Safeer Dec 15 '22 at 13:25
0

Your Player appears to only refer to the paused prop only once when it mounts, in the constructor. Player doesn't react or handle any changes to props.paused when it changes in the parent component and is passed after mounting. Implement componentDidUpdate to react to updates to props.paused to update the component state.

export default class Player extends Component {
  constructor(props) {
    super(props);

    this.state = {
      paused: props.paused,
      totalLength: 1,
      currentPosition: 0,
      selectedTrack: 0,
      repeatOn: false,
      shuffleOn: false,
    };
  }

  ...

  componentDidUpdate(prevProps, prevState) {
    const { paused } = this.props;
    if (!prevState.paused && paused) {
      this.setState({ paused });
    }
  }

  ...

  render() {
    ...
    const video = this.state.isChanging ? null : (
      <Video
        ...
        paused={this.state.paused}
        ...
      />
    );

    return (
      <View style={styles.container}>
        ...
        {video}
      </View>
    );
  }
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Sorry, it did not work. Also the componentDidUpdate is called continuously. I added a console.log statement to it and it keeps displaying 100s of times. – asanas Nov 09 '20 at 09:40
  • @asanas Ack, I think inverted the `paused` prop in the conditional test. What is desired is: If the current `state.paused` is false and a new `props.paused` is true, then update `state.paused` to be true. – Drew Reese Nov 09 '20 at 09:46
  • Ok, now my video is permanently paused. Even if I click the play button, it sets back to pause. Can I contact you? Can we engage professionally? – asanas Nov 09 '20 at 09:50
  • @asanas This is common issue with duplicated state (trying to synchronize them) and it's nearly 2am for me, I can't engage now. I'll take another look in about 6 hours. – Drew Reese Nov 09 '20 at 09:54
  • getDerivedStateFromProps would probably be more appropriate than updating state in `componentDidUpdate` – satya164 Nov 09 '20 at 17:19
  • @DrewReese did you get a chance to look at it? Sorry, if I'm bothering you. – asanas Nov 10 '20 at 01:04