“”
Video Calls with React Native and Twilio

Rest API

View more techs

Video Calls in React Native backed by Twilio

 

Reynaldo Rodríguez

Reynaldo Rodríguez

May 19, 2020

 

Twilio is widely known within the software development field as the pioneer in Cloud Communications Platforms. It provides a powerful set of programmable communication services like voice, video, SMS, AI bots, and most recently, emails (after the acquisition of Sendgrid). In the development process, these services allow us to work on features like phone verification, account recovery, in-app chat, or even in-app video calls.

In this article, we are going to be building a React Native demo app with an in-app video call functionality provided by Twilio Programmable Video. This will require existing knowledge on React, React Native, Cocoapods, and React Native Navigation.

Twilio provides both JavaScript and IOS/Android SDKs. But when talking about React Native, there is no direct support from Twilio. We can use the JavaScript SDK for some services but for others this isn’t possible, as this relies heavily on browser APIs, and that could not be replicated by developing a polyfill or a shim. Another alternative would be to port the native Android/IOS SDK to React Native. For this demo we will use this package: Twilio Video (WebRTC) for React Native.

The package, which is currently active and maintained by the community, implements Twilio’s WebRTC video functionality of the Native IOS/Android Twilio SDK and exposes it to the JavaScript bridge to be used on the React Native project.

Let’s get started: the first thing to do is to sign up for a trial on Twilio and verify both email and phone number. After that, go to the programmable video dashboard and click on Show API Credentials. From here, copy and save the Account SID. Then, in the programmable video API keys section, click on the plus button to generate new API keys to be used on our custom server to provide user authentication on the frontend before accessing the video call. Save these keys securely for later.

Video Calls in React Native backed by Twilio

Now that we are done with the Twilio configuration, let’s initialize the React Native project and install React Navigation, dotenv, and the Twilio package:

npx react-native init reactNativeVideoCall && cd reactNativeVideoCall && yarn add @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view react-native-dotenv react-native-permissions https://github.com/blackuy/react-native-twilio-video-webrtc

As we added react-native-dotenv, we need to register it on the babel.config.js to avoid issues while running:

module.exports = {
  presets: [
    'module:metro-react-native-babel-preset',
    // We add the following code
    'module:react-native-dotenv',
    // End of added code
  ],
};

Regarding the IOS platform configuration and native dependencies, before we install the pods, we need to increment the IOS target to 11 on the Podfile. This is necessary because Twilio’s Native Video SDK only has support for iOS 11.0+:

// We add the following code
platform :ios, '11.0'
// End of added code
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

We also need to set the required permissions on the app by adding the react-native-permissions dependency on the Podfile and then adding the permissions request text in the Info.plist:

pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'


// We add the following code
permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec"
pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone.podspec"
// End of added code

target 'reactNativeVideoCallTests' do
UIViewControllerBasedStatusBarAppearance
  
  // We add the following code
  NSCameraUsageDescription
  We require your permission to access the camera while in a video call
  NSMicrophoneUsageDescription
  We require your permission to access the microphone while in a video call
  // End of added code


Now we are ready to install the pod dependencies and start the app. React Native will autolink them to the IOS project, so there is no need to worry about anything else.

npx pod-install ios && npx react-native run-ios


Video Calls in React Native backed by Twilio

On Android, besides giving the required permissions, we need to do extra steps, because we need to link the Twilio package manually. To add the permissions, we add the following to the AndroidManifest.xml:

// We add the following code
// End of added code


To add the Twilio package, we add the reference in the file android/settings.gradle as follows:

rootProject.name = 'reactNativeVideoCall'
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':react-native-twilio-video-webrtc'
// We add the following code
project(':react-native-twilio-video-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-twilio-video-webrtc/android')
// End of added code
include ':app'


On the android/build.gradle, we also increase the minimum supported Android version, due to a known bug when compiling the latest Twilio native package:

ext {
  buildToolsVersion = "28.0.3"
  minSdkVersion = 24
  compileSdkVersion = 28
  targetSdkVersion = 28
}


Also, on the android/app/build.gradle, we tell gradle to implement this package:

if (enableHermes) {
  def hermesPath = "../../node_modules/hermes-engine/android/";
  debugImplementation files(hermesPath + "hermes-debug.aar")
  releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
  implementation jscFlavor
}
// We add the following code
implementation project(':react-native-twilio-video-webrtc')
// End of added code


Finally, we expose the native dependency to React Native by adding it in the MainApplication.java:

import java.util.List;
// We add the following code
import com.twiliorn.library.TwilioPackage;
// End of added code

public class MainApplication extends Application implements ReactApplication {
@Override
protected List getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  // We add the following code
  packages.add(new TwilioPackage());
  // End of added code
  return packages;
}


Now we can start the app on Android as follows:

npx react-native run-android


Once we have the default app up and running on both platforms with the required dependencies, we’re ready to add our server, which will be serving Twilio client tokens based on users who want to join the video call. We do this by creating a new folder called server and initializing a NodeJS project inside it, with a few dependencies required to compile, serve a REST endpoint, and add the Twilio logic to fetch a user access token:

mkdir server && cd server && npm init -y && yarn add express nodemon dotenv twilio @babel/core @babel/node @babel/preset-env


To get the server up and running, we need to create the following:
A server/.babelrc file to configure Babel:

{
  "presets": [
    "@babel/preset-env"
  ]
}


A start script in the package.json:

"scripts": {
    // We add the following code
    "start": "nodemon --exec babel-node index.js",
    // End of added code
    "test": "echo \"Error: no test specified\" && exit 1"
  },


A server/.env file to set the Twilio API key we generated earlier, along with the Twilio Account SID and the service port:

PORT=3000
ACCOUNT_SID=AC6da012b9c9437214aa91418b1cacdecc
API_KEY_SID=SKda3b13f6259ee6d1a75a9a1b62e754d7
API_KEY_SECRET=Kq8fwUUKbQ893r5s6BlQkUwPZMpJ78qp


Finally, we add the server logic to a server/index.js file to get our endpoint functionality:

import 'dotenv/config';
import express from 'express';

import twilio from 'twilio';
import ngrok from 'ngrok';
const AccessToken = twilio.jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;

const app = express();

app.get('/getToken', (req, res) => {
  if (!req.query || !req.query.userName) {
    return res.status(400).send('Username parameter is required');
  }
  const accessToken = new AccessToken(
    process.env.ACCOUNT_SID,
    process.env.API_KEY_SID,
    process.env.API_KEY_SECRET,
  );

  // Set the Identity of this token
  accessToken.identity = req.query.userName;

  // Grant access to Video
  var grant = new VideoGrant();
  accessToken.addGrant(grant);

  // Serialize the token as a JWT
  var jwt = accessToken.toJwt();
  return res.send(jwt);
});

app.listen(process.env.PORT, () =>
  console.log(`Server listening on port ${process.env.PORT}!`),
);

ngrok.connect(process.env.PORT).then((url) => {
  console.log(`Server forwarded to public url ${url}`);
});


Basically, we are exposing a single GET endpoint called getToken, which receives a userName parameter and generates and returns an access token for this user with video grants on our video service.

To run the server, we just call the start script. That will give us the public URL of the REST API we need to use on the frontend:

yarn start
yarn run v1.21.1
$ nodemon --exec babel-node index.js
[nodemon] 2.0.3
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `babel-node index.js`
Server listening on port 3000!
Server forwarded to public url https://ff949db3.ngrok.io
[nodemon] restarting due to changes...
[nodemon] starting `babel-node index.js`
Server listening on port 3000!
Server forwarded to public url https://3290ad79.ngrok.io


Now, moving to the React Native part: in order to integrate this server, let’s create an .env file on the root folder with the following:

API_URL=https://3290ad79.ngrok.io


Then we override the App.js with the following:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */
import 'react-native-gesture-handler';
import React, {useState, useRef, useEffect, useContext} from 'react';
import {
  StyleSheet,
  View,
  Text,
  StatusBar,
  TouchableOpacity,
  TextInput,
  Alert,
  KeyboardAvoidingView,
  Platform,
  ScrollView,
  Dimensions,
} from 'react-native';

import {API_URL} from 'react-native-dotenv';
import {
  checkMultiple,
  request,
  requestMultiple,
  PERMISSIONS,
  RESULTS,
} from 'react-native-permissions';

import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

import {
  TwilioVideoLocalView,
  TwilioVideoParticipantView,
  TwilioVideo,
} from 'react-native-twilio-video-webrtc';

const Stack = createStackNavigator();
const initialState = {
  isAudioEnabled: true,
  status: 'disconnected',
  participants: new Map(),
  videoTracks: new Map(),
  userName: '',
  roomName: '',
  token: '',
};

const AppContext = React.createContext(initialState);

const dimensions = Dimensions.get('window');

const App = () => {
  const [props, setProps] = useState(initialState);

  return (
    <>;
       
      <StatusBar barStyle="dark-content" />
        
        <AppContext.Provider value={{props, setProps}}>
        <NavigationContainer>
        <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Video Call" component={VideoCallScreen} />
        </Stack.Navigator>
        </NavigationContainer>
        </AppContext.Provider>
       
</>
  );
};

const HomeScreen = ({navigation}) => {
  const {props, setProps} = useContext(AppContext);

  const _checkPermissions = (callback) => {
    const iosPermissions = [PERMISSIONS.IOS.CAMERA, PERMISSIONS.IOS.MICROPHONE];
    const androidPermissions = [
      PERMISSIONS.ANDROID.CAMERA,
      PERMISSIONS.ANDROID.RECORD_AUDIO,
    ];
    checkMultiple(
      Platform.OS === 'ios' ? iosPermissions : androidPermissions,
    ).then((statuses) => {
      const [CAMERA, AUDIO] =
        Platform.OS === 'ios' ? iosPermissions : androidPermissions;
      if (
        statuses[CAMERA] === RESULTS.UNAVAILABLE ||
        statuses[AUDIO] === RESULTS.UNAVAILABLE
      ) {
        Alert.alert(
          'Error',
          'Hardware to support video calls is not available',
        );
      } else if (
        statuses[CAMERA] === RESULTS.BLOCKED ||
        statuses[AUDIO] === RESULTS.BLOCKED
      ) {
        Alert.alert(
          'Error',
          'Permission to access hardware was blocked, please grant manually',
        );
      } else {
        if (
          statuses[CAMERA] === RESULTS.DENIED &&
          statuses[AUDIO] === RESULTS.DENIED
        ) {
          requestMultiple(
            Platform.OS === 'ios' ? iosPermissions : androidPermissions,
          ).then((newStatuses) => {
            if (
              newStatuses[CAMERA] === RESULTS.GRANTED &&
              newStatuses[AUDIO] === RESULTS.GRANTED
            ) {
              callback && callback();
            } else {
              Alert.alert('Error', 'One of the permissions was not granted');
            }
          });
        } else if (
          statuses[CAMERA] === RESULTS.DENIED ||
          statuses[AUDIO] === RESULTS.DENIED
        ) {
          request(statuses[CAMERA] === RESULTS.DENIED ? CAMERA : AUDIO).then(
            (result) => {
              if (result === RESULTS.GRANTED) {
                callback && callback();
              } else {
                Alert.alert('Error', 'Permission not granted');
              }
            },
          );
        } else if (
          statuses[CAMERA] === RESULTS.GRANTED ||
          statuses[AUDIO] === RESULTS.GRANTED
        ) {
          callback && callback();
        }
      }
    });
  };

  useEffect(() => {
    _checkPermissions();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    
      
        
          
            User Name
             setProps({...props, userName: text})}
            />
          
          
            Room Name
             setProps({...props, roomName: text})}
            />
          
          
             {
                _checkPermissions(() => {
                  fetch(`${API_URL}/getToken?userName=${props.userName}`)
                    .then((response) => {
                      if (response.ok) {
                        response.text().then((jwt) => {
                          setProps({...props, token: jwt});
                          navigation.navigate('Video Call');
                          return true;
                        });
                      } else {
                        response.text().then((error) => {
                          Alert.alert(error);
                        });
                      }
                    })
                    .catch((error) => {
                      console.log('error', error);
                      Alert.alert('API not available');
                    });
                });
              }}>
              <Text style={styles.buttonText}>Connect to Video Call</Text>
            </TouchableOpacity>
          </View>
        </View>
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

const VideoCallScreen = ({navigation}) => {
  const twilioVideo = useRef(null);
  const {props, setProps} = useContext(AppContext);

  useEffect(() => {
    twilioVideo.current.connect({
      roomName: props.roomName,
      accessToken: props.token,
    });
    setProps({...props, status: 'connecting'});
    return () => {
      _onEndButtonPress();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const _onEndButtonPress = () => {
    twilioVideo.current.disconnect();
    setProps(initialState);
  };

  const _onMuteButtonPress = () => {
    twilioVideo.current
      .setLocalAudioEnabled(!props.isAudioEnabled)
      .then((isEnabled) => setProps({...props, isAudioEnabled: isEnabled}));
  };

  const _onFlipButtonPress = () => {
    twilioVideo.current.flipCamera();
  };

  return (
    
      {(props.status === 'connected' || props.status === 'connecting') && (
        
          {props.status === 'connected' && (
            
              {Array.from(props.videoTracks, ([trackSid, trackIdentifier]) => (
                
              ))}
            
          )}
        
      )}
      
        
          End
        
        
          
            {props.isAudioEnabled ? 'Mute' : 'Unmute'}
          
        
        
          Flip
        
      
      
       {
          setProps({...props, status: 'connected'});
        }}
        onRoomDidDisconnect={() => {
          setProps({...props, status: 'disconnected'});
          navigation.goBack();
        }}
        onRoomDidFailToConnect={(error) => {
          Alert.alert('Error', error.error);
          setProps({...props, status: 'disconnected'});
          navigation.goBack();
        }}
        onParticipantAddedVideoTrack={({participant, track}) => {
          if (track.enabled) {
            setProps({
              ...props,
              videoTracks: new Map([
                ...props.videoTracks,
                [
                  track.trackSid,
                  {
                    participantSid: participant.sid,
                    videoTrackSid: track.trackSid,
                  },
                ],
              ]),
            });
          }
        }}
        onParticipantRemovedVideoTrack={({track}) => {
          const videoTracks = props.videoTracks;
          videoTracks.delete(track.trackSid);
          setProps({...props, videoTracks});
        }}
      />
    
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'lightgrey',
    flexDirection: 'row',
  },
  form: {
    flex: 1,
    alignSelf: 'center',
    backgroundColor: 'white',
    borderRadius: 10,
    margin: 20,
    flexDirection: 'column',
    justifyContent: 'space-between',
  },
  formGroup: {
    margin: 10,
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  button: {
    padding: 10,
    backgroundColor: 'blue',
    borderRadius: 5,
  },
  buttonText: {
    color: 'white',
    textAlign: 'center',
  },
  textInput: {
    padding: 5,
    borderRadius: 5,
    borderWidth: 1,
    borderColor: 'lightgrey',
  },
  callContainer: {
    flex: 1,
  },
  callWrapper: {
    flex: 1,
    justifyContent: 'center',
  },
  remoteGrid: {
    flex: 1,
  },
  remoteVideo: {
    flex: 1,
  },
  localVideo: {
    position: 'absolute',
    right: 5,
    bottom: 50,
    width: dimensions.width / 4,
    height: dimensions.height / 4,
  },
  optionsContainer: {
    position: 'absolute',
    paddingHorizontal: 10,
    left: 0,
    right: 0,
    bottom: 5,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
});

export default App;


This code serves an App with two screens:

- The first screen handles permissions to access the camera and microphone for both platforms. It will also request a user name and room name, and generate a proper user access token to be used on the video call when redirecting to the next screen.

- The second screen implements the Twilio WebRTC package with the user access token and the selected room name. A scaled local camera feed is displayed on the bottom right, along with some action buttons that can disable, mute, or flip the camera. After the other party joins the call, their camera feed will be displayed on the screen below the local feed and action buttons.

Before running the app, remember that the IOS/Android simulator does not provide a camera integration, so in order to test properly, we should run the app in the actual devices. We can do this by executing the following commands with the connected devices: For IOS:

npx react-native run-ios --device


For Android:

npx react-native run-android --deviceId NWB0218C27004191


The deviceId can be obtained by checking connected devices with adb:

$ adb devices
List of devices attached
51cddafa	device
NWB0218C27004191	device


On the first screen, we need to enter a user name and a room name to join, and then press Connect to Video Call. Just remember to use the same room name on both devices so they can see each other.

Video Calls in React Native backed by Twilio


Then, on the second screen, the library will connect to Twilio Video Service using the access token and it will trigger the respective call events that are handled within our app.



Video Calls in React Native backed by Twilio


To summarize, we just created a simple React Native app to illustrate video call capability using Twilio, as well as highlighting this great Twilio WebRTC library for React Native.

Something to keep in mind is that this is far from a production architecture. This is because some things like user access token generation, user connections, room creation, and those extra flows around the call and users should be handled properly on the backend.

The source code created on this tutorial can be found on https://github.com/ReyRod/reactNativeVideoCall

Reynaldo Rodríguez

Reynaldo Rodríguez

Reynaldo is a Computer Science graduate with eight years of experience in web and mobile development working with the latest technologies. As a senior full-stack developer in the VAIRIX team, he works on a variety of projects for the USA. Reynaldo is currently in charge of designing solutions for our projects using technologies like React, React Native and Node.

Contact us

Ready to get started? Use the form below or give us a call to meet our team and discuss your project and business goals.
We can’t wait to meet you!


Follow Us
See our client reviews