Video player with controllers using React Native

If you are already familiar with React you can skip to Video Player Component part.

React is a View library which is developed by Facebook to improve their development experience. Later on they added React-Native to develop native mobile applications using same principles of React.

Their motto is “Learn once, write everywhere” which was controversy to other frameworks philosophy of “Write once, run everywhere”. As it claims that regardless of where you write your code, whether it is browser or mobile app, you should use the same principles to develop applications. Due to different requirements of different platforms, it doesn’t try to run the same code on each platform. But it makes it possible for even a single developer to write a web app or an android/ios app by just applying the principles of React.

“Learn once, write everywhere”

In React you create reusable Components which represents your view. You just plug components to each other and React ensures that whenever data changes due to user actions or any other events, view will be updated properly.

For ensuring correct rendering of views it uses `state` and `props` in components. Basically state is the object which holds the data of your app, and whenever some part of the state changes, corresponding components will be updated in React. Whereas `props` are used for passing data between components and they are read-only data. Data passing in React is uni-directional, so parent components passes data into child components using `props`. Children notify parents using again props by calling corresponding props function. And in most cases the parent components should keep the state and notify its children by passing `props`, having state in children components is not advised as parent component should be aware of its children’s state. By having a unidirectional data flow you will have an easy to follow app, which will improve your development experience.


Video Player Component

In this tutorial, I will use React-Native to create a video player component with controllers for changing scene of video using a progress bar. You can access the complete code from https://github.com/cubbuk/react-native-video-player

Setup React-Native on Ubuntu

Before starting our project we need to install bunch of libraries.

Requirements

So basically following the steps mentioned in official website of React Native will get you far enough to start your application. You will need toinstall the following libraries.

  • Node
  • React Native Command Line Tools
  • Install Android Studio (JDK and JRE, follow the guide on React Native web site for correct installation)
  • Watchman (for keeping track of changed files during your development, React Native is great for debugging your app as it has the ability of hot loading and live reload which makes it possible to see each change in your code without redeploying your app)
  • Gradle Daemon
  • Android Emulator (Genymotion)

After installation of the requirements, you can start a new project using `react native cli` with the following command:

react-native init ReactNativeVideoPlayer

This will create a new project directory named `ReactNativeVideoPlayer` and create necessary `Android` and `IOS` files to run your application. For this tutorial I will be using `Genymotion` as an emulator. I created a new emulator and while it is running, you can start your app by the following commands

react-native run-android
react-native start

I put a single command into `package.json` file to run this commands sequentially. You can start your app by the following command too:`npm run start-android`

{
"name": "ReactNativeVideoPlayer",
"version": "0.0.1",
"private": true,
"scripts": {
"start-android": "node node_modules/react-native/local-cli/cli.js run-android; node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"lodash": "^4.13.1",
"react": "^15.1.0",
"react-native": "^0.27.0-rc2",
"react-native-video": "^0.8.0-rc"
}
}

Your application should be deployed on your emulator by now, but most probably you are seeing a red screen similar to the below one:

Start error on Ubuntu, just press Reload and your project will run properly on the emulator

Just press reload and your app should be deployed properly now. I couldn’t figure out why I am getting this error, if anyone has a clue please let me know.

As you can see from the `package.json` we will be using `react-native-video` package to play a video file. This is a great library developed by Brent Vatne. I highly recommend to read projects of him as it provides great examples for implementing features such as animations in your app.

Now lets start by looking at `index.android.js`. You may already noticed there is 2 index files in the project, one named `index.android.js` and the other is `index.ios.js`. React Native provides an easy way to seperate files to run under different platforms, files suffixed with `android` will be run under android and files ends with `ios` will be run under IOS. If you will share the same exact file under 2 platforms you may omit platform names and just name your component `myComponent.js`.

Actually our index file defines a component named `ReactNativeVideoPlayer` which returns `VideoPlayer` component. In React every component has to have a `render` function which renders the component. This function will be invoked whenever the state of this component changes.

`VideoPlayer` component receives one prop named `video`. This is the video file we will play in our app. You can find this video file under the directory `android/app/src/res/raw/`. VideoPlayer component will use this prop to play it.

Now lets look at VideoPlayer Component. This component is a stateful component as it keeps the `paused` value in its state. This value is passed to the `Video` component to play or pause the given video file. Lets checkout the render function:

let {onClosePressed, video} = this.props;
let {currentTime, duration, paused} = this.state;
const completedPercentage = this.getCurrentTimePercentage(currentTime, duration) * 100;
return <View style={styles.fullScreen} key={this.state.key}>
<TouchableOpacity style={styles.videoView}
onPress={this.playOrPauseVideo.bind(this, paused)}>
<Video ref={videoPlayer => this.videoPlayer = videoPlayer}
onEnd={this.onVideoEnd.bind(this)}
onLoad={this.onVideoLoad.bind(this)}
onProgress={this.onProgress.bind(this)}
source={{uri:video.uri}}
paused={paused}
volume={0}
resizeMode="contain"
style={styles.videoContainer}/>
{paused &&
<Image style={styles.videoIcon}
source={require("../assets/images/play-icon.png")}/>}
</TouchableOpacity>
<View style={[styles.controller]}>
<View
style={[styles.progressBar]}>
<ProgressController duration={duration}
currentTime={currentTime}
percent={completedPercentage}
onNewPercent={this.onProgressChanged.bind(this)}/>
</View>
</View>
</View>;

As you can see this component keeps the currentTime, duration and paused value of the video in its state. And it also has video as a prop. But it also has a prop named `onClosePressed` which will be called whenever the close button is clicked. This prop will let the parent state now that this component wants to be closed. As you can see here parent component is responsible for what to do in when the close button is pressed. So in one application you may navigate to another page or just hide this component in another one. This is totally responsibility of the parent scope, which makes this component to be reusable in different parts of the application.

Video component plays the video and by passing paused props to this component we can pause or play the video. By also using onProgress prop we set the currentTime of the video. Now we can keep track of how much of the video is played so far, and use this percentage value to render a slider view which will be our ProgressController.

As we wrap our Video component under a TouchableOpacity component, we can press on video player to pause or play it. We achieve this by assigning `this.playOrPauseVideo.bind(this, paused)` to onPressed prop.

<TouchableOpacity style={styles.videoView}
onPress={this.playOrPauseVideo.bind(this, paused)}>

Here we are passing a function as a prop and by using `bind` we are currying our function. That means we are passing the paused value as a parameter to the function, so when this function called we will be sure that the current paused value in the render function will be used in playOrPauseVideo function. Here also we set the context of `this` to the component itself by using bind again, which enables us to use setState function inside playOrPauseVideo.

So far we can easily play or pause video, but we don’t have any control over seeking video in our app. For doing so we will be using ProgressController.

<ProgressController duration={duration}
currentTime={currentTime}
percent={completedPercentage}
onNewPercent={this.onProgressChanged.bind(this)}/>

We are passing the duration, currentTime and completed percentage of video as props . We also pass `onProgressChanged` function as a prop to this be component to be notified whenever the slider is dragged in this component. This is a very basic example of how components interact with each other. Progress controller renders slider according to the `completedPercentage` prop. Then whenever it changes the percentage by using slider, it notifies its parent about this event and the parent computes new currentTime and seeks the videoPlayer to the computed time. Then after setting the state with new values ProgressController will re-render itself to display correct current time.

Its time to see the details of ProgressController:

let {moving} = this.state;
let {currentTime, duration, percent} = this.props;
return <View style={styles.view}>
<Text style={[styles.timeText, {marginRight: 10}]}>{this.formatSeconds(currentTime)}</Text>
<View style={styles.barView}
onLayout={this.onLayout.bind(this)} {...this.holderPanResponder.panHandlers}>
<View style={{flex: 1, flexDirection: "row", top: moving ? radiusOfActiveHolder : radiusOfHolder}}>
<TouchableOpacity style={[styles.line, {flex: percent, borderColor: "blue"}]}
onPress={this.onLinePressed.bind(this)}/>
<TouchableOpacity style={[styles.line, {flex: 100 - percent, borderColor: "white"}]}
onPress={this.onLinePressed.bind(this)}/>
</View>
<Animated.View style={this.getHolderStyle()}/>
</View>
<Text style={[styles.timeText, {marginLeft: 10}]}>{this.formatSeconds(duration)}</Text>
</View>

Here we need to be aware of few methods which are provided to us by react-native.

<View style={styles.barView}
onLayout={this.onLayout.bind(this)} {...this.holderPanResponder.panHandlers}>

You may be wondering what is this onLayout prop is about? Well this prop is called by View component whenever the view is mounted or the layout of the view is changed. And this method returns an event object which tells us the width and height of this view along with many other features such as x and y position of this view on the screen. So now we know the width of our view and we can use our percentage value to display the current time as an indicator on the progress bar.

One another thing here is that panHandlers. PanResponder is an API provided by react-native to handle touch events. It has many methods and in this example we used some of them. Using this API we can track touch events such as moving of a finger or pinching a view etc.

componentWillMount() {
this.holderPanResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (e, gestureState) => {
let {slideX} = this.state;
this.setState({moving: true});
slideX.setOffset(slideX._value);
slideX.setValue(0);
},
onPanResponderMove: (e, gestureState) => {
let totalX = this.state.slideX._offset + gestureState.dx;
let newPercent = (totalX / this.state.width) * 100;
this.notifyPercentChange(newPercent, true);
Animated.event([
null, {dx: this.state.slideX}
])(e, gestureState);
},
onPanResponderRelease: (e, gesture) => {
this.state.slideX.flattenOffset();
let newPercent = (this.state.slideX._value / this.state.width) * 100;
this.setState({moving: false});
this.notifyPercentChange(newPercent, false);
}
});
}

PanHandler methods

  • onStartShouldSetPanResponder: This function is invoked when the touch starts, by returning true we are declaring that we want to capture this event. This is necessary as anything on the screen can capture a touch event. So if you return false this panHandler will not capture any touch event. This method and other pan handler methods returns event and gestureState as parameters. Event gives information about the position of the touch, whereas `gestureState` gives information about how many touch is active on the view, or how much dragging happened in X or Y coordinate. So this enables us to capture event when a particular type of touch happens. For example you may only capture an event when the absolute value of gestureState.dy is more than 10 i.e. capture only if there is a movement in Y direction
  • onMoveShouldSetPanResponder: I am not very sure of this function, but I believe this is similar to onStartShouldSetPanResponder, but this function is invoked not on start of a touch but after a movement.
  • onPanResponderGrant: This function is invoked when the panHandler becomes the responder of the current touch. So this is a point where you make initializations before starting an event. Here I am setting moving in my state to true for indicating user is dragging the slider. I need to mention another API of React Native at this point which is called Animated. Using Animated API we can easily make animations in our apps and even better we can combine pan handlers with animated values to apply user touch events to our views. For doing so in this example I created an Animated.Value which is stored in the state as slideX. So here when the pan responder granted, I am setting offset of the slideX to the value of the slideX and setting the value of slideX to zero. By doing so I can track how much dragging happened on SlideX for each different touch event.
  • onPanResponderMove: This function will be invoked on each movement until the touch is released. Using gestureState (gestureState.dx) parameter we can learn how much x is dragged in this current touch. Here I add the offset of slideX to gestureState.dx to compute exactly how much slideX is moved in x direction. Using width of the view which is computed from onLayout method, we can learn exactly what percentage of the video is skipped so far. Animated.event is a function for easily mapping event and gesture state parameter to the tracked animated values. Here it is setting the dx value of gesture state to the slideX value. It doesn’t do anything with event parameter. for setting pageX of event to slideX you may use the following:
 Animated.event([
{nativeEvent: {pageX: this.state.slideX}}, null])(e, gestureState);
  • onPanResponderRelease: When the touch is released this function will be called. Here you can do computations to finalize your animation. By calling flattenOffset on slideX, we are adding offset to the value of slideX to show the correct postion of animation. Here we also call notifyPercentageChange to let our parent view to seek the video to correct position.

There are many other pan events which will not be covered here, but for advanced animations you may need them. Checking out PanResponder and Animation API , Gesture Responder System and Animations articles are crucial for understanding Animations in React Native.

Ok we are now aware of user touch events but we need a way to reflect these events on the screen with a smooth transition. If we use this.setState to reflect each change you will see lots of flickering and it won’t be a nice user experience. Therefore we will use Animated.View along with Animated.Value to reflect these changes to our view.

<Animated.View style={this.getHolderStyle()}/>

Animated.View will ensure that layout changes will be applied smoothly. But we need to map touch events to our View. We will use interpolation for mapping touch events, here we have a one to one relation. By using clamp we are making sure that, output values will not exceed the given input range. There may be a shorter way to express this but I couldn’t find a way to express it, if you know it please let me know.

If the user is moving the slider, the holder will have a larger radius. In this example I need animation only on X coordinate, so using Animated.Value was enough. If you need animation on both coordinates you may use Animated.ValueXY which is described very well in the following link:

getHolderStyle() {
let {moving, slideX, width} = this.state;

if (width > 0) {
var interpolatedAnimation = slideX.interpolate({
inputRange: [0, width],
outputRange: [0, width],
extrapolate: "clamp"
});
return [styles.holder, moving && styles.activeHolder,
{transform: [{translateX: interpolatedAnimation}]}
];
} else {
return [styles.holder];
}
}

Here is a screenshot of the example:

As I mentioned above the full source of this example can be found on github. Just clone it and install dependencies by npm install and play with it. If you have any questions or improvements about this code please let me know.


Lastly I am currently looking for young fellows to work with in my company 20 Satır. If you want to learn more about React or looking for an internship, just drop me a mail on info@20satir.com.