Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
How to build a video streaming app with React Native and Mux

How to build a video streaming app with React Native and Mux

Dec 15, 2021

This article is written by Kolawole Mangabo

In this tutorial, you’ll learn how to use React Native, the mobile framework, the Mux Video API, and Codemagic for better CI/CD pipelines to build a video streaming application.

If you are familiar with applications such as Instagram, TikTok, or YouTube, you already know about video streaming.

Such applications can be quite complex to make. That’s why we’ll be using the Mux Video API to abstract the complexities of video streaming and write the logic of the API directly.

Mux API configuration

To use the Mux API, we’ll need API secrets from Mux. We’ll be adding assets and then retrieving API keys. Let’s begin!

Log in and go to the dashboard.

Adding an asset

Click on Add a video file.

Running the request for adding the video file

Once it’s done, click on Run Request. We can now generate the API tokens we’ll use to make requests to the API.

Go to the settings, and click on Generate new token.

Generating the tokens

Fill in the required details and generate the tokens.

Details for creating tokens

Your tokens will be generated now.

Generating tokens

We’ll be using these keys in a .env file, which will be ignored by .gitignore.

The project: What our app will look like

For this tutorial, we’ll be building a simple React Native application with two screens.

  • The first screen will be used to register a video and list all our assets from the Mux API;
  • The second screen will play the content and show information about the video.

Building the back end: Setting up a mini server with Flask

But first, we’ll need to set up some back end — a server. We’ll be creating this server using Flask — a lightweight and fast Python framework for building web applications or APIs. As we’ll only need a few endpoints here, Flask is definitely a great solution for our project. We could also have used Django, for example, but it comes with a lot of tools we won’t be using.

But why use a server and not just the default request module of JavaScript in React Native, such as Axios? First of all, it’s not really a good practice to put env keys on the front end, as they can be easily accessed, and this can result in a sensitive data leak. Using these keys on the server side significantly reduces the risk. So, a mini server is a better solution in our case.

We’ll be making requests to the mini server from the React Native application instead of directly hitting the Mux API on the React Native side. Now, let’s go.

First of all, let’s create a virtual environment.

cd flask-api
virtualenv --python=/usr/bin/activate venv
source venv/bin/activate

Once it’s done, create and install Flask.

pip install Flask python-dotenv

Also, let’s install the Mux Python package.

pip install git+https://github.com/muxinc/mux-python.git

Now, create a file .env, and add the following information.

MUX_TOKEN_ID="<mux_token_id>"
MUX_TOKEN_SECRET="<mux_token_secret>"

The next step is to create an app.py, and then we’ll start writing the code logic.

import os
from flask import Flask, request, jsonify
import mux_python
from mux_python.rest import ApiException
import json 

# Authentication Setup
configuration = mux_python.Configuration()
configuration.username = os.environ.get('MUX_TOKEN_ID')
configuration.password = os.environ.get('MUX_TOKEN_SECRET')

# API Client Initialization
assets_api = mux_python.AssetsApi(mux_python.ApiClient(configuration))
playback_ids_api = mux_python.PlaybackIDApi(mux_python.ApiClient(configuration))

app = Flask(__name__)

We are importing the Flask utilities, as well as some utilities from the mux_python package. This will help us initialize the clients to make requests to the Mux API.

As a first step here, let’s write a route to create a new video asset.

@app.route('/assets', methods=['POST'])
def create_asset():
    input_json = request.get_json(force=True)
    url = input_json.get('url')
    if url is None or not isinstance(url, str):
        abort(400, {'url': "This field is required"})
    input_settings = [mux_python.InputSettings(url=url)]
    try:
        create_asset_request = mux_python.CreateAssetRequest(input=input_settings)
        create_asset_response = assets_api.create_asset(create_asset_request)
    except ApiException:
        abort(400, {'message': "An error has occurred"})

    asset = create_asset_response.data
    new_data = {
        "id": asset.id,
        "status": asset.status,
        "created_at": asset.created_at,
        "duration": asset.duration,
        "max_stored_resolution": asset.max_stored_resolution,
        "max_stored_frame_rate": asset.max_stored_frame_rate,
        "aspect_ratio": asset.aspect_ratio
    }
    return json.dumps(new_data)

This function will create an asset. We are getting the body in the request and performing some verification. Once the check is done, we are using the API client we defined earlier to make a request to create a Mux asset.

Start running the server using this command.

flask run

Now that we can create assets, we’ll use Insomnia or Postman to make a POST request at http://127.0.0.1:5000/assets.

GET all assets

Let’s add endpoints — one to retrieve all assets and another one for one asset.

@app.route('/assets', methods=['GET'])
def list_assets(): 
    try:
        list_assets_response = assets_api.list_assets()
    except ApiException:
        abort(400, {'message': "An error has occurred"})
    new_data = map(lambda asset: {
        "id": asset.id,
        "status": asset.status,
        "created_at": asset.created_at,
        "duration": asset.duration,
        "max_stored_resolution": asset.max_stored_resolution,
        "max_stored_frame_rate": asset.max_stored_frame_rate,
        "aspect_ratio": asset.aspect_ratio
    }, list_assets_response.data)
    return jsonify(list(new_data))

@app.route('/assets/<string:asset_id>', methods=['GET'])
def get_asset(asset_id):
    try:
        asset_object_response = assets_api.get_asset(asset_id)
        asset = asset_object_response.data
    except ApiException:
        return "An error has occurred"
    new_data = {
        "id": asset.id,
        "status": asset.status,
        "created_at": asset.created_at,
        "playback_id": asset.playback_ids[0].id,
        "duration": asset.duration,
        "max_stored_resolution": asset.max_stored_resolution,
        "max_stored_frame_rate": asset.max_stored_frame_rate,
        "aspect_ratio": asset.aspect_ratio
    }
    return jsonify(new_data)

if __name__ == '__main__':
    app.run()

These two functions also use some functions provided by the mux_python package.

  • The assets_api.list_assets() function will return a Python object containing some attributes and the list of assets.
  • The assets_api.get_asset(asset_id) function will return a Python object containing some attributes as well as data about the asset we are looking for.

All right, the server is ready. As you can see, this was done very quickly with Flask. Now, let’s move to the front-end part with React Native.

Building the front end with React Native

React Native is a framework that allows you to build native mobile apps using JavaScript or TypeScript. It provides a slick, smooth, and responsive UI while allowing you to maintain one codebase. If you are interested in working with Mux using other mobile frameworks, we also have a similar post for Flutter.

Let’s create the React Native project.

npx react-native init MuxReactNative

Once it’s done, move the directory, build the app on your device, and start the server.

yarn run ios // or android
yarn start 

Adding packages

Let’s install the packages we’ll be using for this small project.

yarn add swr axios react-native-elements react-native-video react-native-safe-area-context react-native-vector-icons react-native-notifier

Now, let’s briefly describe what we need each of the packages for:

  • swr : React Hook for data fetching;
  • axios: JavaScript packages to make requests and fetch data;
  • react-native-elements: a React Native library that comes with reusable UI components;
  • react-native-video: a React Native component to display videos;
  • react-native-notifier: a React Native package to show warnings or notifications inside the application.

Now, we can start coding. Create a directory named src at the root of the project.

mkdir src

Application logic

Configurations

In the src dir, add a file named config.js. This file will contain the configurations and utilities we’ll be using for this project.

import axios from 'axios';
import {Notifier, NotifierComponents} from 'react-native-notifier';

// API for sending videos
export const muxBaseUrl = 'https://api.mux.com';

// API server running on localhost
export const muxServerUrl = 'http://localhost:5000';

// API for generating thumbnails of a video
export const muxImageBaseUrl = 'https://image.mux.com';

// API for streaming a video
export const muxStreamBaseUrl = 'https://stream.mux.com';

// Received video file format
export const videoExtension = 'm3u8';

// Thumbnail file type and size
export const imageTypeSize = 'thumbnail.jpg?time=5&width=200';

// Content Type used in API calls
export const contentType = 'application/json';

export const fetcher = url =>
  axios.get(url).then(res => {
    console.log(res.headers);
    return res.data;
  });

export const notify = {
  notify(title: string, description: string, type: string) {
    Notifier.showNotification({
      title,
      description,
      Component: NotifierComponents.Alert,
      componentProps: {
        alertType: type,
      },
    });
  },
  error(title: string, description?: string) {
    notify.notify(title, description, 'error');
  },
  success(title: string, description?: string) {
    notify.notify(title, description, 'success');
  },
};

export function getFormattedDate(timestamp) {
  const date = new Date(timestamp * 1000);

  return (
    date.getDate() +
    '/' +
    (date.getMonth() + 1) +
    '/' +
    date.getFullYear() +
    ' ' +
    date.getHours() +
    ':' +
    date.getMinutes() +
    ':' +
    date.getSeconds()
  );
}

With the local server, you may encounter issues while trying to connect to the API. To resolve these issues, install ngrok, and expose the 5000 port by running ngrok http 5000. Then, replace the generated URL with the one in the file.

Let’s create the file that will contain our navigator, StackNavigator.js.

import * as React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import HomeScreen from '../Home';
import {VideoStreaming} from '../VideoStreaming';

const Stack = createStackNavigator();

const StackNavigator = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen
        options={{
          title: 'Home',
          headerStyle: {
            backgroundColor: '#5000ca',
          },
          headerTitleStyle: {
            color: '#fff',
          },
        }}
        name="Home"
        component={HomeScreen}
      />
    </Stack.Navigator>
  );
};

export default StackNavigator;

And now, let’s wrap the application into the Navigator as well as the Notifier so that we can easily trigger notifications. We’ll be working on App.js.

import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import StackNavigator from './src/StackNavigator';
import { NotifierWrapper } from 'react-native-notifier';

export default function App() {
  return (
    <NotifierWrapper>
      <NavigationContainer>
        <StackNavigator />
      </NavigationContainer>
    </NotifierWrapper>
  );
}

First of all, let’s create a file, a layout we’ll use for our components.

import * as React from 'react'; 
import {SafeAreaView, StyleSheet} from 'react-native';

const Layout = props => {
  return <SafeAreaView style={styles.container}>{props.children}</SafeAreaView>;
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
});

export default Layout;

Home screen

Once that’s done, create a file named Home.js. First of all, let’s make the necessary requests to fetch all the assets. We’ll also need to write the function to create a new asset.

import React, {useState} from 'react';
import Layout from './Layout';
import {Input, Text, Button} from 'react-native-elements';
import {
  TouchableOpacity,
  View,
  Dimensions,
  ScrollView,
  Image,
} from 'react-native';
import {
  fetcher,
  getFormattedDate,
  imageTypeSize,
  muxImageBaseUrl,
  muxServerUrl,
  notify,
} from './config';
import useSWR from 'swr';
import axios from 'axios';

const windowHeight = Dimensions.get('window').height;

function HomeScreen({navigation}) {
  const [videoURL, setVideoURL] = useState('');

  const navigateToStreamingScreen = (videoId: string) => {
    navigation.navigate('VideoStreaming', {
      videoId: videoId,
    });
  };

  const assets = useSWR(`${muxServerUrl}/assets`, fetcher);

  const assetsData = assets.data || [];

  const handleVideoLinkSubmit = videoUrl => {
    axios
      .post('`${muxServerUrl}/assets', {url: videoUrl})
      .then(res => {
        notify.success('Video added.');
      })
      .catch(error => notify.error('An error has occurred'));
  };

return ();

}

export default HomeScreen;

We can write the view for this component now.

... 
return (
    <Layout>
      <View
        style={{
          paddingHorizontal: '2%',
        }}>
        <Text
          style={{
            paddingHorizontal: '2%',
          }}
          h3>
          Upload
        </Text>
        <Input
          label={'Video URL'}
          containerStyle={{
            maxWidth: '100%',
          }}
          placeholder={'https://video.mp4'}
          onChangeText={setVideoURL}
        />
        <Button
          title="Send"
          buttonStyle={{backgroundColor: '#5000ca'}}
          onPress={() => handleVideoLinkSubmit(videoURL)}
        />
      </View>

      <ScrollView style={{margin: '3%'}}>
        {assetsData.map((asset, index) => (
          <TouchableOpacity
            style={{
              height: windowHeight * 0.25,
              marginVertical: '1%',
            }}
            onPress={() => navigateToStreamingScreen(asset.id)}>
            <Text
              style={{
                marginVertical: '2%',
                textAlign: 'center',
                backgroundColor: 'gray',
                color: 'white',
              }}>
              id: {asset.id}
            </Text>
            <View
              style={{
                flexDirection: 'row',
                height: '60%',
              }}>
              <Image
                style={{
                  height: 150,
                  width: 150,
                }}
                source={{
                  uri: `${muxImageBaseUrl}/${asset?.playback_id}/${imageTypeSize}`,
                }}
              />
              <View
                style={{
                  width: '50%',
                  height: windowHeight * 0.2,
                  padding: '2%',
                }}>
                <View
                  style={{
                    flexDirection: 'row',
                    alignItems: 'baseline',
                  }}>
                  <Text
                    style={{
                      fontSize: 18,
                      fontWeight: '700',
                    }}>
                    Duration:
                  </Text>
                  <Text
                    style={{
                      paddingBottom: '1%',
                    }}>
                    {' '}
                    {asset.duration} seconds
                  </Text>
                </View>
                <View
                  style={{
                    flexDirection: 'row',
                    alignItems: 'baseline',
                  }}>
                  <Text
                    style={{
                      fontSize: 18,
                      fontWeight: '700',
                    }}>
                    Status:
                  </Text>
                  <Text
                    style={{
                      paddingBottom: '1%',
                    }}>
                    {' '}
                    {asset.status}
                  </Text>
                </View>
                <View
                  style={{
                    flexDirection: 'row',
                    alignItems: 'baseline',
                  }}>
                  <Text
                    style={{
                      fontSize: 18,
                      fontWeight: '700',
                    }}>
                    Created at:
                  </Text>
                  <Text
                    style={{
                      paddingBottom: '1%',
                    }}>
                    {' '}
                    {getFormattedDate(parseInt(asset.created_at))}
                  </Text>
                </View>
              </View>
            </View>
          </TouchableOpacity>
        ))}
      </ScrollView>
    </Layout>
  );
...

After we’ve finished all of these, the screen should look like this.

Home page

This is the first screen of our app, where we select the content that we want to stream. The other screen we need to build for our Mux + React Native app is the video streaming screen.

Video streaming screen

In the same directory, create a file named VideoStreaming.js.

import React from 'react';
import Video from 'react-native-video';
import {
  fetcher,
  getFormattedDate,
  muxServerUrl,
  muxStreamBaseUrl,
  videoExtension,
} from './config';
import useSWR from 'swr';
import {View} from 'react-native';
import {Divider, Text} from 'react-native-elements';
import Layout from './Layout';

export const VideoStreaming = ({route, navigation}) => {
  const {videoId} = route.params;

  const asset = useSWR(`\`${muxServerUrl}/assets/${videoId}`, fetcher);
  const assetData = asset.data;

return ();

}

Notice the usage of route in the props of this component. This will help us get the parameters that have been passed when navigating to this screen.

We can now write the view using the Video component.

...
return (
    <Layout>
      {assetData ? (
        <>
          <Video
            source={{
              uri: `${muxStreamBaseUrl}/${assetData.playback_id}.${videoExtension}`,
            }}
            paused={false}
            style={{
              height: '40%',
              marginBottom: '2%',
            }}
            repeat={true}
          />
          <View
            style={{
              paddingHorizontal: '4%',
            }}>
            <View>
              <Text h4>Asset ID</Text>
              <Text>{assetData.id}</Text>
            </View>

            <Divider
              width={1}
              style={{
                marginVertical: '5%',
              }}
            />
          </View>
          <View
            style={{
              paddingHorizontal: '4%',
            }}>
            <View>
              <Text h4>Created at</Text>
              <Text>{getFormattedDate(parseInt(assetData.created_at))}</Text>
            </View>

            <Divider
              width={1}
              style={{
                marginVertical: '5%',
              }}
            />
          </View>
          <View
            style={{
              paddingHorizontal: '4%',
            }}>
            <View>
              <Text h4>Duration</Text>
              <Text>{assetData.duration}</Text>
            </View>

            <Divider
              width={1}
              style={{
                marginVertical: '5%',
              }}
            />
          </View>
          <View
            style={{
              paddingHorizontal: '4%',
            }}>
            <View>
              <Text h4>Max Resolution</Text>
              <Text>{assetData.max_stored_resolution}</Text>
            </View>

            <Divider
              width={1}
              style={{
                marginVertical: '5%',
              }}
            />
          </View>
        </>
      ) : (
        <Text>Loading</Text>
      )}
    </Layout>
  );
...

Let’s add this screen to our Navigator.

import {VideoStreaming} from '../VideoStreaming';
...
      <Stack.Screen
        options={{
          title: 'Video Streaming',
          headerStyle: {
            backgroundColor: '#5000ca',
          },
          headerTitleStyle: {
            color: '#fff',
          },
          headerTintColor: '#fff',
        }}
        name="VideoStreaming"
        component={VideoStreaming}
      />
...

Your video streaming screen should look like this.

Adding video streaming screen

Great! We are done integrating Mux with React Native, and the code for our app is ready.

What’s next?

Building the project with Codemagic

Codemagic helps you write CI/CD pipelines for mobile applications, including building, testing, and publishing your apps. For React Native, it’s one of the simplest ways to build your apps for both Android and iOS, as well as do code signing for both platforms and publish the app to Google Play Store and Apple App Store. However, for the sake of brevity, we’ll only build the Android .apk in this post. If you want a detailed getting started guide for React Native and Codemagic CI/CD, check out this blog post.

First, you’ll have to log in to your Codemagic account, or sign up if you don’t have one.

Sign up

Once this is done, add a new application, and select the source control service you want to use to save your source code. I am working with GitHub in this example, but you can choose whatever service you want.

Once that’s done, you can select your project in the next step. Make sure that the project type is set to React Native App.

After that, you’ll be brought to an editor.

Basically, all we need to do is create a codemagic.yaml file containing the CI/CD logic we are looking for at the root of your project.

For the debug version of our app, it will look like this.

# Check out https://docs.codemagic.io/getting-started/building-a-react-native-app/ for more information
# Please review and update values in curly braces

workflows:
    react-native:
        name: React Native App
        environment:
            node: 16.13.1
        scripts:
            - npm install
            - echo "sdk.dir=$HOME/programs/android-sdk-macosx" > "$CM_BUILD_DIR/android/local.properties"
            - |
                # build Android
                cd android
                ./gradlew assembleDebug
        artifacts:
            - android/app/build/outputs/**/**/*.apk

This will create a debug version of your mobile application that is ready to be installed and used. But you’ll probably want to go with a release version of your mobile application.

Let’s modify the workflow and adapt it to our needs. But first of all, let’s talk about the required changes.

- |
                # build Android
                cd android
                ./gradlew assembleRelease

The build process will follow the path as defined in the build.gradle file. This will require code signing, which you can read about here. For iOS, the process is a bit more complicated, but we’ve explained it here.

When the release build command starts running, it’ll use the release signing details. The build.gradle will contain the release signing details used by Codemagic when the build is running.

signingConfigs {
        debug {
            storeFile file('debug.keystore')
            storePassword 'android'
            keyAlias 'androiddebugkey'
            keyPassword 'android'
        }        release {
            if (System.getenv()['CI']) { // CI=true is exported by Codemagic
                storeFile file(System.getenv()['CM_BUILD_DIR'] + '/keystore.jks')
                storePassword System.getenv()['CM_KEYSTORE_PASSWORD']
                keyAlias System.getenv()['CM_KEY_ALIAS']
                keyPassword System.getenv()['CM_KEY_PASSWORD']
            }
                   else {
                def keystorePropertiesFile = rootProject.file('key.properties')
                def keystoreProperties = new Properties()
                keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile file(keystoreProperties['storeFile'])
                storePassword keystoreProperties['storePassword']
                   }
        }
    }

Two things are happening here:

  • We check if the CI environment variable has been declared. If this is the case, we fetch the location of the keystore and appropriate signing details from the environment.

  • Or we simply use the local key.properties file to fetch the details.

The next step is to encrypt the environment variables for YAML configuration.

storePassword=android
keyPassword=yourpassword
keyAlias=androiddebugkey
storeFile=keystore.keystore

Encrypting build dependencies

On the project configuration page, you can see a tab option named Environment variables. Create a group name called mux-app (you can change it according to your needs), and add the values according to your key.properties file.

Let’s quickly talk about some environment variables we’ll be adding here:

CM_KEYSTORE_PASSWORD: storePassword
CM_KEY_PASSWORD: keyPassword
CM_KEY_ALIAS: keyAlias

These are just the values for the application environment variables for an Android application. These values have already been created when signing the application.

  • CM_KEYSTORE_PASSWORD: This represents your keystore password.
  • CM_KEY_PASSWORD: If you have created a keystore alias password, you can put it here.
  • CM_KEY_ALIAS: This represents your keystore alias username.

After that, you should have a codemagic.yaml file that looks like this.

workflows:
    react-native:
        name: React Native App
        environment:
            groups:
                - mux-app    
            vars:
                CM_KEYSTORE_PASSWORD: Encrypted(...)
                CM_KEY_PASSWORD: Encrypted(...)
                CM_KEY_ALIAS: Encrypted(...)
            node: 16.13.1
        scripts:
            - yarn install
            - react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/app.bundle
            - echo "sdk.dir=$HOME/programs/android-sdk-macosx" > "$CM_BUILD_DIR/android/local.properties"
            - |
                chmod -R 777 $CM_BUILD_DIR
                echo $CM_KEYSTORE | base64 --decode > $CM_BUILD_DIR/keystore.jks
                # build Android
                cd android
                ./gradlew assembleRelease

Conclusion

In this article, we’ve covered how to easily integrate Mux with React Native. The Mux API is very large and interesting to play with. Don’t hesitate to explore their docs.

And finally, you can find the code of this project with a working codemagic.yaml file on GitHub here.


Kolawole Mangabo is a fullstack engineer working with Python and JavaScript. Currently busy building things in a foodtech company, he’s also a technical writer, covering Django, React and React Native on his personal blog.

Related articles

Latest articles

Show more posts