Tuesday, June 8, 2021

Building a React Native Video Chat App Using Agora

 

Adding video streaming functionality in a React Native application from scratch can be a huge task. Maintaining low latency, load balancing, and managing user event states can be tedious. On top of that, we have to maintain cross-platform compatibility.

There’s an easy way to do this. In this tutorial, we’ll walk through building a React Native video chat app by using the Agora Video SDK. We’ll go over the structure, setup, and execution of the app before diving into the logistics. You can get a cross-platform video call app going in a few simple steps within a matter of minutes.

We’ll be using Agora RTC SDK for React Native for the example. I’m using v3.1.6 at the time of writing.

Creating an Account with Agora

Sign up at https://console.agora.io and log in to the dashboard.

Building a React Native Video Chat App Using Agora - Screenshot #1
The project management tab on the website

Navigate to the Project List tab under the Project Management tab, and create a project by clicking the blue Create button. (When prompted to use App ID + Certificate, select only App ID.) Retrieve the App ID, it will be used to authorize your requests while you’re developing the application.

Note: This guide does not implement token authentication, which is recommended for all RTE apps running in production environments. For more information about token-based authentication in the Agora platform, please refer to this guide: https://docs.agora.io/en/Video/token?platform=All%20Platforms.

Structure of Our Example

This is the structure of the application that we’re building:

.
├── android
├── components
│ └── Permission.ts
│ └── Style.ts
├── ios
├── App.tsx
.

Let’s run the app

You’ll need to have the LTS version of Node.js and NPM installed:

  • Make sure you have an Agora account, set up a project, and generated an App ID.
  • Download and extract the ZIP file from the master branch.
  • Run npm install to install the app dependencies in the unzipped directory.
  • Navigate to ./App.tsx and enter the App ID that we generated as appId: "<YourAppId>"
  • If you’re building for iOS, open a terminal and execute cd ios && pod install.
  • Connect your device, and run npx react-native run-android / npx react-native run-ios to start the app. Give it a few minutes to build and launch the app.
  • Once you see the home screen on your mobile (or emulator), click the start call button on the device. (The iOS simulator does not support the camera, so use a physical device instead.)

That’s it. You should have a video call going between the two devices. The app uses channel-x as the channel name.

How the App Works

App.tsx

This file contains all the core logic of our video call:

import React, {Component} from 'react'
import {Platform, ScrollView, Text, TouchableOpacity, View} from 'react-native'
import RtcEngine, {RtcLocalView, RtcRemoteView, VideoRenderMode} from 'react-native-agora'
import requestCameraAndAudioPermission from './components/Permission'
import styles from './components/Style'
/**
* @property peerIds Array for storing connected peers
* @property appId
* @property channelName Channel Name for the current session
* @property joinSucceed State variable for storing success
*/
interface State {
appId: string,
token: string,
channelName: string,
joinSucceed: boolean,
peerIds: number[],
}
...
view rawApp.tsx hosted with ❤ by GitHub

We start by writing the import statements. Next, we define an interface for our application state containing:

  • appId: our Agora App ID
  • token: token generated to join the channel
  • channelName: name for the channel (users on the same channel can communicate with each other)
  • joinSucceed: boolean to store if we’ve connected successfully
  • peerIds: an array to store the UIDs of other users in the channel
...
export default class App extends Component<Props, State> {
_engine?: RtcEngine
constructor(props) {
super(props)
this.state = {
appId: YourAppId,
token: YourToken,
channelName: 'channel-x',
joinSucceed: false,
peerIds: [],
}
if (Platform.OS === 'android') {
// Request required permissions from Android
requestCameraAndAudioPermission().then(() => {
console.log('requested!')
})
}
}
componentDidMount() {
this.init()
}
/**
* @name init
* @description Function to initialize the Rtc Engine, attach event listeners and actions
*/
init = async () => {
const {appId} = this.state
this._engine = await RtcEngine.create(appId)
await this._engine.enableVideo()
this._engine.addListener('Warning', (warn) => {
console.log('Warning', warn)
})
this._engine.addListener('Error', (err) => {
console.log('Error', err)
})
this._engine.addListener('UserJoined', (uid, elapsed) => {
console.log('UserJoined', uid, elapsed)
// Get current peer IDs
const {peerIds} = this.state
// If new user
if (peerIds.indexOf(uid) === -1) {
this.setState({
// Add peer ID to state array
peerIds: [...peerIds, uid]
})
}
})
this._engine.addListener('UserOffline', (uid, reason) => {
console.log('UserOffline', uid, reason)
const {peerIds} = this.state
this.setState({
// Remove peer ID from state array
peerIds: peerIds.filter(id => id !== uid)
})
})
// If Local user joins RTC channel
this._engine.addListener('JoinChannelSuccess', (channel, uid, elapsed) => {
console.log('JoinChannelSuccess', channel, uid, elapsed)
// Set state variable to true
this.setState({
joinSucceed: true
})
})
}
...
view rawApp.tsx hosted with ❤ by GitHub

We define a class-based component: the _engine variable will store the instance of the RtcEngine class imported from the Agora SDK. This instance provides the main methods that can be invoked by our application for using the SDK’s features.

In the constructor, we set our state variables and request permissions for the camera and the microphone on Android. (We use a helper function from permission.ts, as described below.) When the component is mounted, we call the initfunction, which initializes the RTC Engine using the App ID. It also enables the video by calling the enableVideo method on our engine instance. (The SDK can work in audio-only mode if this is omitted.)

The init function also adds event listeners for various events in the video call. For example, the UserJoined event gives us the UID of a user when they join the channel. We store this UID in our state to use to render their videos later.

Note: If there are users connected to the channel before we have joined, a UserJoined event is fired for each user after they join the channel.

...
/**
* @name startCall
* @description Function to start the call
*/
startCall = async () => {
// Join Channel using null token and channel name
await this._engine?.joinChannel(this.state.token, this.state.channelName, null, 0)
}
/**
* @name endCall
* @description Function to end the call
*/
endCall = async () => {
await this._engine?.leaveChannel()
this.setState({peerIds: [], joinSucceed: false})
}
render() {
return (
<View style={styles.max}>
<View style={styles.max}>
<View style={styles.buttonHolder}>
<TouchableOpacity
onPress={this.startCall}
style={styles.button}>
<Text style={styles.buttonText}> Start Call </Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.endCall}
style={styles.button}>
<Text style={styles.buttonText}> End Call </Text>
</TouchableOpacity>
</View>
{this._renderVideos()}
</View>
</View>
)
}
_renderVideos = () => {
const {joinSucceed} = this.state
return joinSucceed ? (
<View style={styles.fullView}>
<RtcLocalView.SurfaceView
style={styles.max}
channelId={this.state.channelName}
renderMode={VideoRenderMode.Hidden}/>
{this._renderRemoteVideos()}
</View>
) : null
}
_renderRemoteVideos = () => {
const {peerIds} = this.state
return (
<ScrollView
style={styles.remoteContainer}
contentContainerStyle={{paddingHorizontal: 2.5}}
horizontal={true}>
{peerIds.map((value, index, array) => {
return (
<RtcRemoteView.SurfaceView
style={styles.remote}
uid={value}
channelId={this.state.channelName}
renderMode={VideoRenderMode.Hidden}
zOrderMediaOverlay={true}/>
)
})}
</ScrollView>
)
}
}
view rawApp.tsx hosted with ❤ by GitHub

Next, we have functions to start and end the call. The joinChannel method takes in the token, the channel name, optional information, and an optional UID. (If you set UID as 0, the system automatically assigns a UID for the local user.)

We define the render function for displaying buttons to start and end the call and to display our local video feed as well as the remote users’ video feeds. We define the _renderVideos functions that render our video feeds. They are rendered in a scrollview using the peerIds array.

To display the local user’s video feed, we use the <RtcLocalView.SurfaceView> component, which takes in channelId and renderMode as props. User’s connecting to the same channelId can communicate with each other. The renderMode prop is used to either fit the video in a view or zoom to fill the view.

To display the remote user’s video feed, we use the <RtcLocalView.SurfaceView> component from the SDK, which takes in the UID of the remote user along with channelId and renderMode.

Permission.ts

import {PermissionsAndroid} from 'react-native'
/**
* @name requestCameraAndAudioPermission
* @description Function to request permission for Audio and Camera
*/
export default async function requestCameraAndAudioPermission() {
try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
])
if (
granted['android.permission.RECORD_AUDIO'] === PermissionsAndroid.RESULTS.GRANTED
&& granted['android.permission.CAMERA'] === PermissionsAndroid.RESULTS.GRANTED
) {
console.log('You can use the cameras & mic')
} else {
console.log('Permission denied')
}
} catch (err) {
console.warn(err)
}
}
view rawPermission.ts hosted with ❤ by GitHub

We’re exporting a function to request camera and microphone permissions from the OS on Android.

Style.ts

import {Dimensions, StyleSheet} from 'react-native'
const dimensions = {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
}
export default StyleSheet.create({
max: {
flex: 1,
},
buttonHolder: {
height: 100,
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-evenly',
},
button: {
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: '#0093E9',
borderRadius: 25,
},
buttonText: {
color: '#fff',
},
fullView: {
width: dimensions.width,
height: dimensions.height - 100,
},
remoteContainer: {
width: '100%',
height: 150,
position: 'absolute',
top: 5
},
remote: {
width: 150,
height: 150,
marginHorizontal: 2.5
},
noUserText: {
paddingHorizontal: 10,
paddingVertical: 5,
color: '#0093E9',
},
})
view rawStyle.ts hosted with ❤ by GitHub

The Style.ts file contains the styling for the components.

That’s how easy it is to build a video calling app. You can refer to the Agora React Native API Reference to see methods that can help you quickly add more features like muting the camera and microphone, setting video profiles, and audio mixing.

No comments:

Post a Comment

Technology’s generational moment with generative AI: A CIO and CTO guide

 Ref:  A CIO and CTO technology guide to generative AI | McKinsey 1. Determine the company’s posture for the adoption of generative AI As us...