Pause video in flatlist when out of view
Asked Answered
R

3

12

I have a flatlist showing videos. I want that when a video goes out of the view it should be paused. I am maintaining the pause state in each of the Posts component.

class Posts extends React.PureComponent {

constructor() {
 super()
 this.state = {
   pause: true,
 }

 return(){
  <Video
    pause={this.state.pause}
    //other props
  />
 }

}

I am using react-native-video.

I have tried using onViewableItemsChanged prop of Flatlist but it doesn't change the state.

I tried this . But it doesn't seem to work for me.

How should I proceed ?

Reverent answered 23/2, 2019 at 9:1 Comment(0)
T
14

Here is a possible solution using react-native-inviewport. This dependency is only a single index file that contains a component that has a callback when view is in the viewport. It could easily be modified to suit your needs.

I have constructed a very simple app that has a FlatList. The 11th item in the FlatList is a video. That should mean that the video is off the screen when the App renders so the viddeo won't be playing, once the video comes fully into the viewport it should then start playing.

App.js

import * as React from 'react';
import { Text, View, StyleSheet, FlatList } from 'react-native';
import Constants from 'expo-constants';
import VideoPlayer from './VideoPlayer';

export default class App extends React.Component {

  state = {
    data: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
  }

  renderItem = ({item, index}) => {
    if (index === 10) {
      return <VideoPlayer />
    } else {
      return (
        <View style={{height: 100, backgroundColor: '#336699', justifyContent: 'center', alignItems: 'center'}}>
          <Text>{index}</Text>
        </View>
      )
    }
  }
  keyExtractor = (item, index) => `${index}`;

  render() {
    return (
      <View style={styles.container}>
        <FlatList
          data={this.state.data}
          renderItem={this.renderItem}
          keyExtractor={this.keyExtractor}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    backgroundColor: '#ecf0f1',
    padding: 8,
  }
});

VideoPlayer.js

This is a component that contains the Video component. The video is wrapped in the InViewPort component that has a callback function. The callback returns true when the component it surrounds is completely in the viewport and false when it is not fully in the viewport. The callback calls this.handlePlaying which in turn calls either this.playVideo or this.pauseVideo depending on the boolean value.

import React, {Component} from 'react';
import { View, StyleSheet } from 'react-native';
import { Video } from 'expo-av';
import InViewPort from './InViewPort';
//import InViewPort from 'react-native-inviewport; // this wouldn't work in the snack so I just copied the file and added it manually.

export default class VideoPlayer extends React.Component {

  pauseVideo = () => {
    if(this.video) {
      this.video.pauseAsync();
    }
  }

  playVideo = () => {
    if(this.video) {
      this.video.playAsync();
    }
  }

  handlePlaying = (isVisible) => {
    isVisible ? this.playVideo() : this.pauseVideo();
  }

  render() {
      return (
        <View style={styles.container}>
         <InViewPort onChange={this.handlePlaying}>
          <Video
            ref={ref => {this.video = ref}}
            source={{ uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4' }}
            rate={1.0}
            volume={1.0}
            isMuted={false}
            resizeMode="cover"
            shouldPlay
            style={{ width: 300, height: 300 }}
          />
          </InViewPort>
        </View>
      )
  }  
}

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center'
  }
});

Here is a snack showing it working https://snack.expo.io/@andypandy/video-only-playing-when-in-viewport

I should point out that if the video is not fully in the viewport then it will not play. I am sure some tweaking could be done to react-native-inviewport so that it would play the video if it was partially in the viewport if that is what you wanted, perhaps by passing the height of the video to the InViewPort component.

Ternan answered 23/2, 2019 at 18:30 Comment(10)
Above plays all the videos in flatlist at same time not one by one on focusElegist
It will play any video that is completely visible to the user. So if you have three videos that are on the screen, and completely visible, then it will play all three. You could update the InViewPort.js that is contained in the snack to define what you mean by "on focus" as currently it checks the whole screen, but it could be modified to check a portion of the screen.Ternan
thanks for reply. The problem is it plays even the videos out of my screen the videos in the bottom of the flatlist. I have to mention that I am not using expo.Elegist
That's strange because it doesn't do that for me.Ternan
@Ternan my flat list consists of all the video's so, this makes all videos to playConceit
@PradnyanandMilindPohare if a video is fully visible then the above solution will make it play automatically. It is clear that you have further questions. I would suggest writing up the relevant parts into a new question so that you can clearly express everything that you need to do, as the comments on SO do not really allow a detailed discussion to take place. For one, code does not format nicely here; and secondly, you are limited to 600 characters, so it makes it hard to express oneself fully.Ternan
@Andrew, will your solution work if there are multiple videos in Flatlist horizontal view? I tried your snack, it worked there not in my project. I'm using import Video from 'react-native-video'; not import { Video } from 'expo-av'; is there any difference in this? Please clarify, I'm a newbie in RN.Ingrain
Had the same issue and this package solved it, thank you @Andrew!Ursola
Thank you so much. This method is working. I was struggling with scrollView etc but this actually works. ThanksDivagate
InViewPort only works on androidThornie
V
7

here is how i simply did the trick inside my card component that have video

<Video ...
    paused={currentIndex !== currentVisibleIndex}
/>

both currentIndex and currentVisibleIndex are passed the component from the FlatList parent

my FlatList pass the renderItem index as currentIndex

<FlatList
    data={[...]}
    renderItem={({ item, index }) => (
    <GalleryCard
        {...item}
        currentIndex={index}
        currentVisibleIndex={currentVisibleIndex}
        />
        )}
    onViewableItemsChanged={this.onViewableItemsChanged}
    viewabilityConfig={{
        viewAreaCoveragePercentThreshold: 90
    }}

finally my this is how to calculate currentVisibleIndex please make sure to read viewabilityConfig

onViewableItemsChanged = ({ viewableItems, changed }) => {
    if (viewableItems && viewableItems.length > 0) {
        this.setState({ currentVisibleIndex: viewableItems[0].index });
    }
};

please let me know if this is helpful

Verily answered 13/8, 2019 at 2:32 Comment(1)
would you please check this scenario. #62469332Elizbeth
R
0

I finally did this using redux. Not sure whether it is the right way.

Home.js

_renderItem = ({item, index}) => <Posts item={item} />

viewableItemsChanged = (props) => {
    let {changed} = props;
    let changedPostArray = []; 
    changed.map((v,i) => {
      let {isViewable, item} = v;
      if(!isViewable) {
        let {post_id, type} = item;
        if(type === 1)
          changedPostArray.push(post_id);
      }
    });
    if(changedPostArray.length != 0)
      this.props.sendPostToPause(changedPostArray);
  }

render() {
    return(
      <View style={{flex: 1}} >
          <FlatList
            ref={(ref) => this.homeList = ref}
            refreshing={this.state.refreshing}
            onRefresh={async () => {
              await this.setState({refreshing: true, postsId: [0], radiusId: 0, followingId: 0});
              this.getPosts();
            }}
            onEndReached={async () => {
              if(!this.state.isEnd) {
                await this.setState({isPaginate: true})
                this.getPosts()
              }
            }}
            onEndReachedThreshold={0.2}
            removeClippedSubviews={true}
            contentContainerStyle={{paddingBottom: 80}}
            data={this.state.followersRes}            
            renderItem={this._renderItem}
            viewabilityConfig={this.viewabilityConfig}
            onViewableItemsChanged={this.viewableItemsChanged}
          />
        </View>
        <Footer callback={this.scrollToTopAndRefresh} />
      </View>
    )
}
export default connect(null, {sendPostToPause})(Home);

Posts.js

class Posts extends React.PureComponent {

constructor(){
 super()
 this.state: {
  pause: false
 }
}

componentDidUpdate(prevProps) {
    if(prevProps != this.props) {
      this.props.postIdArray.map((v) => {
        if(v === prevProps.item.post_id) {
          this.setState({pause: true})
        }
      })
    }
  }

render(){
 return(
  <Video
    pause={this.state.pause}
    //Other props
  />
 )
}


}

const mapStateToProps = (state) => {
  const {postId} = state.reducers;
  return {
    postIdArray: postId
  }
}

export default connect(mapStateToProps, {sendPostToPause})(withNavigation(Posts));

Whenever the viewableItemsChanged is trigger I am adding the changed posts id in an array and calling the action with the array of post ids. In the Posts component I am checking if the post ids match, if so I am setting the pause state to true.

Reverent answered 23/2, 2019 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.