React Native - Pause Video on Navigation - reactjs

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.

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

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>
);
}

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>
);
}
}

Related

Component starts to break after some time - react

I want to create a component that will take any image and make it spin in circles.
I managed to do so but appears I have an issue with setting the interval cleanup function as it starts to switch quickly from one state to another and the picture spins like crazy.
This is the spinner component
import classes from './Spinner.module.css'
import { useState , useEffect} from 'react';
const Spinner = (props) =>{
const [Timer, setTimer] = useState('5');
useEffect(() => {
const interval = setInterval(() => {
let newT;
if(Timer==='5'){
newT='1';
}
else{
newT='5';
}
setTimer(newT);
}, 2000);
console.log(Timer);
return clearInterval(interval);
}, [Timer])
return <img style={{animation: `${classes.spin} ${Timer}s linear infinite`}} src={props.img} alt="img"/>
};
export default Spinner ;
Spin CSS :
#keyframes spin {
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
When the interval created cleanup function it did not work, since it was not in a function.
Change was in the return function of the useEffect component.
const Spinner = (props) => {
const [Timer, setTimer] = useState("5");
useEffect(() => {
const interval = setInterval(() => {
setTimer((prevState) => prevState ==='5' ? '1' : '5');
}, 2000);
console.log(Timer);
return ()=>{clearInterval(interval)};
}, [Timer]);
return (
<img
style={{ animation: `${classes.spin} ${Timer}s linear infinite` }}
src={props.img}
alt="img"
/>
);
};

Auto focus Input within React Native Viewpager

I'm using a React Native Viewpager to take in user entry, and move to the next page on button press. Important to note that moving to the next page happens on button press, and not by normal scrolling, which is disabled.
The best way I could think to handle this was to have a state on the ViewPager, which would propagate into the child Entries.
ViewPager.tsx:
export default function ViewPager({ route, navigation }) {
const ref: React.RefObject<ViewPager> = React.createRef();
const [currentPage, setCurrentPage] = useState(0);
let setEntryPage = (page: number) => {
ref.current?.setPage(page);
setCurrentPage(page);
}
return (
<View style={{flex: 1}}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={ref}
scrollEnabled={false}
>
{
GlobalStuff.map((entry, index) => {
return (
<Entry
key={index}
index={index}
pagerFocusIndex={currentPage}
pagerLength={quizDeck?.litems.length!}
setEntryPage={setEntryPage}
/>
)
})
}
</ViewPager>
</View>
);
};
Entry.tsx:
export function Entry(props: EntryProps) {
const inputRef: React.RefObject<Input> = React.createRef();
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
return (
<View>
<Input
// ...
ref={inputRef}
/>
<IconButton
icon="arrow-right-thick"
color={colorTheme.green}
onPress={() => {
props.index !== props.pagerLength - 1 ?
props.setEntryPage(props.index + 1) :
props.navigation!.reset({ index: 0, routes: [{ name: recapScreenName as any }] });
}}
/>
// ...
Unfortunately, inputRef appears to be null, and there is probably a better way of achieving what I'm trying to achieve anyway.
Anything in your render loop will be called every time the component renders.
// This is called on every render
const inputRef: React.RefObject<Input> = React.createRef();
// So is this, it's always null
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
Put side effects in effects.
// Untested
const inputRef = useRef();
useEffect(() => {
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
}, [inputRef.current, props.pagerFocusIndex, props.index]);

React shows stale data on player page

I have a video player page that displays trailers.
Question:
The first time when the video page loads, it works perfect! However,
if I select another video, I still see the stale data (even though the
props is passing in correct data). AKA I see Stale data. (But if I refresh the page, then it works fine)
Code:
import React from 'react';
import { useSelector } from 'react-redux';
import Videojs from './video.js';
import Loader from '../components/common/Loader';
const PlayerPage = props => {
const { location } = props;
const selectedMovieData = location.state && location.state.data;
const link = selectedMovieData && selectedMovieData.downloadLink;
const videoJsOptions = {
responsive: true,
preload: 'none',
fluid: true,
width: 1420,
aspectRatio: '16:7',
height: 300,
techOrder: ['chromecast', 'html5'],
controls: true,
sources: [
{
src: link,
type: 'video/mp4'
}
],
};
const { loading } = useSelector(state => state.movieStore);
const classes = useStyles();
return (
<div className={classes.root}>
{loading ? (
<Loader />
) : (
<>
<VideoPlayStyle>
<Videojs {...videoJsOptions} />
</VideoPlayStyle>
</>
)}
</div>
);
};
export default PlayerPage;
Video.js component:
// taken from https://github.com/videojs/video.js/blob/master/docs/guides/react.md
import React from 'react';
import '../styles/video.css';
export default class VideoPlayer extends React.Component {
componentDidMount() {
// instantiate video.js
this.player = window.player = window.videojs(this.videoNode, this.props, function onPlayerReady() {
console.log('VideoJS: onPlayerReady', this);
});
this.player.chromecast();
}
// destroy player on unmount
componentWillUnmount() {
if (this.player) {
this.player.dispose();
}
}
// wrap the player in a div with a `data-vjs-player` attribute
// so videojs won't create additional wrapper in the DOM
// see https://github.com/videojs/video.js/pull/3856
render() {
return (
<div data-vjs-player>
<video ref={node => (this.videoNode = node)} className="video-js" id="play-video" />
</div>
);
}
}
I'm trying to figure out what's going wrong. Any pointers would be appreciated.
I'm using React Hooks.

React navigation header right button

I want add button in react-native header , the button is to mas and unmask password in the page, the problem on click when i change the state to change secureTextEntry value, the icon wont change will keep as the initial value;
the function is working fine but the icon cant change
this.state.secureTextEntry ? "eye" : "eye-slash"
this is the main code
class ChangePasswordScreen extends Component {
constructor(props) {
super(props);
this.state = {
newPassword: null,
currentPassword: null,
confirmPassword: null,
errors: [],
secureTextEntry: true
};
this.maskPassword = this.maskPassword.bind(this)
}
componentDidMount() {
this.props.navigation.setParams({
headerRight: ( < TouchableOpacity onPress = {
() => {
this.maskPassword();
}
} > < Icon style = {
styles.eyeIcon
}
name = {
this.state.secureTextEntry ? "eye" : "eye-slash"
}
size = {
20
}
color = {
Colors.WHITE
}
/></TouchableOpacity > )
})
}
static navigationOptions = ({
navigation
}) => {
return {
// headerTitle: <LogoTitle />,
headerRight: navigation.state.params && navigation.state.params.headerRight,
};
};
maskPassword = () => {
this.setState({
secureTextEntry: !this.state.secureTextEntry
})
}
}
Kinda late, might help someone nevertheless.
If you wish to add a button to the header of a screen, from the screen itself, not the App.js file and you are using a functional component, it goes like this:
import { useNavigation } from '#react-navigation/native'
export default function () {
const nav = useNavigation();
useEffect(() => {
nav.setOptions({
headerRight: () => <Button />,
});
}
}
The problem is this.setState will not re-render header component . if you want to change header right then you have to call setParams again
Try this code in componentDidMount
componentDidMount() {
this.props.navigation.setParams({
headerRight: this.setHeaderRight(this.state.secureTextEntry)
});
}
Set function for header right
setHeaderRight = state => {
//console.log("setHeaderRight", this.state.secureTextEntry);
return (
<TouchableOpacity
onPress={() => {
this.maskPassword();
}}
>
<Icon
style={styles.eyeIcon}
name={state ? "eye" : "eye-slash"}
size={20}
color={Colors.WHITE}
/>
</TouchableOpacity>
);
};
Set header right again when state set
maskPassword = () => {
this.setState({
secureTextEntry: !this.state.secureTextEntry
});
this.props.navigation.setParams({
headerRight: this.setHeaderRight(!this.state.secureTextEntry)
});
};
You are setting a Component as a navigation param on Component mount and passing in a state value at the time the Component mounted.
This param never gets changed or updated again so the navigation header never gets re rendered.
A better way would be to pass the value of state directly as a navigation param and use that in the component that is used directly in the navigationOptions

React Component Props are receiving late. (Meteor JS)

I am working on a react-native and meteor js project.
My problem is that the props received from withTracker() function are only received in componentDidUpdate(prevProps) I don't get them in constructor or componentWillMount.
Another issue is when i pass props directly from parent to child. it receives them late due to which my component does not update
iconGroups prop comes from withTracker() method
and openSection props which i am using in this showGroupIcons()
is passed directly from parent to this component.
I want to open Accordian section that is passed to it via parent. but problem is in componentDidUpdate(prevProps) I am changing state due to which component re-renders.
openSection variable by default value is Zero. when props arrvies it value changes which i required But Accordian does not update.
Below is my code
import React, { Component } from 'react';
import Meteor, { withTracker } from 'react-native-meteor';
import {
View, Image, ScrollView, TouchableOpacity,
} from 'react-native';
import PopupDialog from 'react-native-popup-dialog';
import {Text, Icon, Input, Item, List,} from 'native-base';
import Accordion from 'react-native-collapsible/Accordion';
import { Col, Row, Grid } from 'react-native-easy-grid';
import styles from './styles';
import CONFIG from '../../config/constant';
import {MO} from "../../index";
const staticUrl = '../../assets/img/icons/';
class IconPickerComponent extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: [],
itemName: 'apple1',
activeSections: 0,
showAccordian: true,
accordianData: []
};
}
componentDidUpdate(prevProps) {
if(prevProps.iconGroups !== this.props.iconGroups) {
let images = this.props.iconGroups.map(icon => icon.images);
let flatten = [].concat.apply([], images).map(img => { return {name: img, icon: CONFIG.ICON_URL+img+'.png'} })
this.setState({ filteredItems: flatten, dataSource: flatten, accordianData: this.props.iconGroups });
}
}
componentDidMount() {
this.props.onRef(this);
}
componentWillUnmount() {
this.props.onRef(null);
}
method() {
// this.setState(...this.state,{
// searchText: ''
// })
this.iconPicker.show(); // show icon picker
}
onSearchChange(text) {
this.setState({
showAccordian: !(text.length > 0)
});
const searchText = text.toLowerCase();
const filteredItems = this.state.dataSource.filter((item) => {
const itemText = item.name.toLowerCase();
return itemText.indexOf(searchText) !== -1;
});
this.setState({ filteredItems });
}
onIconSelect(item) {
this.setState({
itemName: item,
});
this.iconPicker.dismiss();
if (this.props.onIconChanged) {
this.props.onIconChanged(item);
}
}
_renderSectionTitle = section => {
return (
<View style={styles.content}>
<Text></Text>
</View>
);
};
_renderHeader = section => {
return (
<View style={styles.accordHeader}>
<Text style={{color: 'white'}}>{this.state.showAccordian} - {section.group}</Text>
<Text>
<Icon style={styles.downArrow} name="ios-arrow-down" />
</Text>
</View>
);
};
_renderContent = section => {
return (
<View style={styles.accordContent}>
{
section.images.map((img, key) => (
<TouchableOpacity onPress={() => this.onIconSelect(img)} key={key}>
<View style={styles.iconsGrid}>
<Image style={styles.image} source={{uri: CONFIG.ICON_URL+ img + '.png'}}/>
</View>
</TouchableOpacity>
))
}
</View>
);
};
_updateSections = activeSections => {
this.setState({ activeSections });
};
hasGroupIcons() {
return this.props.iconGroups.length > 0;
};
showGroupIcons() {
if(this.state.showAccordian){
let openSection;
if(!!this.props.openSection) {
let groupIndex = this.state.accordianData.findIndex(icon => icon.group === this.props.openSection);
if(groupIndex !== -1) {
openSection = groupIndex;
} else {
openSection = 0;
}
} else {
openSection = 0;
}
return(<Accordion
sections={this.state.accordianData}
activeSections={this.state.activeSections}
renderSectionTitle={this._renderSectionTitle}
renderHeader={this._renderHeader}
renderContent={this._renderContent}
onChange={this._updateSections}
initiallyActiveSection={openSection} />);
} else {
return(<View style={{flexWrap: 'wrap', flexDirection: 'row'}}>
{
this.state.filteredItems.map((item, key) => (
<TouchableOpacity onPress={() => this.onIconSelect(item.name)} key={key}>
<View style={styles.iconsGrid}>
<Image style={styles.image} source={{uri: item.icon}}/>
</View>
</TouchableOpacity>
))
}
</View>)
}
};
render() {
return (
<PopupDialog
overlayOpacity={0.8}
overlayBackgroundColor="#414141"
dialogStyle={styles.dialogBox}
containerStyle={styles.dialogContainer}
ref={(popupDialog) => { this.iconPicker = popupDialog; }}
>
<ScrollView>
<View style={styles.dialogInner}>
<Item searchBar rounded style={styles.searchbar}>
<Icon style={styles.searchIcon} name="search" />
<Input onChangeText={this.onSearchChange.bind(this)} style={styles.inputSearch} placeholder="Search" />
</Item>
{
this.hasGroupIcons() && this.showGroupIcons()
}
</View>
</ScrollView>
</PopupDialog>
);
}
}
export default withTracker(params => {
MO.subscribe('ipSubsId3', 'IconGroups');
return {
iconGroups: MO.collection('IconGroups', 'ipSubsId3').find({}),
};
})(IconPickerComponent);
I am new to react. I am assuming when props change component re-renders.
Use this life cycle method
static getDerivedStateFromProps(prevProps, prevState) {
if(prevProps.iconGroups !== this.props.iconGroups) {
let images = this.props.iconGroups.map(icon => icon.images);
let flatten = [].concat.apply([], images).map(img => { return {name: img, icon: CONFIG.ICON_URL+img+'.png'} })
this.setState({ filteredItems: flatten, dataSource: flatten, accordianData: this.props.iconGroups });
}
}
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
Read more about this lifecycle method here
I have fixed this issue. Actually my concepts were not right. I thought props are first received in constructor and componentWillMount. But I get all props in render() and everything works fine i dont have to use any lifecycle method to use props now

Resources