==

Q-Consultation for every industry

Securely hold virtual meetings and video conferences

Learn More>

Want to learn more about our products and services?

Speak to us now

How to Create an App for Video Calls with QuickBlox React Native SDK

Oleksandr Shvetsov
25 Nov 2021
React Native Video App

Note: This blog has been updated since it was originally published in February 2020

Video chat provides a dynamic way to engage your users in real time communication. Improve employee collaboration, energize online social communities, and enhance customer satisfaction with a video calling app. You can learn more about the benefits of video calling from our earlier blog, as well as learn the financial and technical costs involved with creating a video chat app of your own.

To save your developers time and money, QuickBlox has crafted several SDKs that provide pre-built communication features. Our robust video call SDK for React Native allows you to effortlessly add chat and video chat functionality to your app. React Native is an increasingly popular framework for building cross-platform applications because only one app needs to be built that can work across platforms, iOS, Android, and Web.

In this article, the QuickBlox team shows you how to build a React Native video chat application using our powerful QuickBlox React Native SDK, which is free to download and install today. Also, check out our React Native code samples and documentation.

Creating the application backbone

React Native CLI provides an easy way to create a new React Native application:

npx react-native init AwesomeWebRTCApp

react-native init AwesomeWebRTCApp

Once react-native-cli has created a project we need to update the `ios/Podfile`: platform: ios, ‘12.0’ — since the current version of the QuickBlox React Native SDK supports iOS version 12.0 onwards.

We will need the following packages in addition to be preinstalled:

  • quickblox-react-native-sdk – main package of this application
  • redux – to maintain application state
  • react-redux – react bindings for redux
  • redux-persist – to keep application state persistent across reloads
  • @react-native-async-storage/async-storage – as a storage engine for redux-persist
  • redux-logger – helper that will put all actions and store changes in debugger’s console
  • redux-saga – library for making application side effects (i.e. asynchronous things like data fetching and impure things like accessing the cache)
  • react-navigation – routing and navigation for React Native apps
  • react-native-reanimated – dependency of react-navigation
  • react-native-gesture-handler – dependency of react-navigation
  • react-native-screens – dependency of react-navigation
  • final-form – form state management library
  • react-final-form – form state management for React
  • react-native-incall-manager – handling media-routes/sensors/events during a audio/video chat on React Native
  • react-native-flash-message – flashbar and top notification alert utility

To install all the packages, we need to run:

npm install --save quickblox-react-native-sdk redux react-redux redux-persist @react-native-community/async-storage redux-logger redux-saga react-navigation react-native-reanimated react-native-gesture-handler react-native-screens final-form react-final-form react-native-incall-manager react-native-flash-message

npm install --save quickblox-react-native-sdk redux react-redux redux-persist @react-native-community/async-storage redux-logger redux-saga react-navigation react-native-reanimated react-native-gesture-handler react-native-screens final-form react-final-form react-native-incall-manager react-native-flash-message

Project structure

We will use separate folders for components, containers, sagas, and other parts of the application:

  • actionCreators – application’s action creators
  • components – presentational components
  • constants – constants representing actions names
  • containers – container components
  • reducers – application’s reducers
  • sagassagas (API calls, calls to SDK, etc.)
  • store – redux store initialization
  • images – key-value images collection used in this application (imported with require, so can be used as Image.source)
  • QBConfig – object with credentials for QuickBlox SDK initialization
  • theme – app-wide styles (colors, navigation header styles, etc.)

QuickBlox application credentials

In order to use the QuickBlox React Native SDK, we should initialize it with the correct application credentials. To create an application you will need an account – you can register at https://admin.quickblox.com/signup or login if you already have one.

Create your QuickBlox app and obtain app credentials. These credentials will be used to identify your app.

In this app, we will store QuickBlox application credentials in file src/QBConfig.js. So once you have app credentials put them into that file:

export default {
  appId: '',
  authKey: '',
  authSecret: '',
  accountKey: '',
  apiEndpoint: '',
  chatEndpoint: '',
}

Configuring the application

Our application has several points that we should configure. Let’s go over them:

  1. constants – this folder will have only one file (index.js) that will export string constants which we will use in this app (check it out in the repository)
  2. actionCreators – this folder will have several files for different app parts and one file exporting all action creators (check it out in the repository)
  3. reducers – this folder will have several files for different app parts and one file that exports all reducers combined AKA root reducer (check it out in the repository)
  4. sagas – this folder will have several files for different app parts and one file that exports all application sagas combined into one saga AKA root saga (check it out in the repository)
  5. store – this folder will have only one file (index.js) that will export function to set up redux store for application (check it out in the repository)

Once we’ve created folders for each of the items listed above, we can configure our entry point at src/index.js:

import React from 'react'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { enableScreens } from 'react-native-screens'

import App from './App'
import Loading from './components/Loading'
import configureStore from './store'
import rootSaga from './sagas'

enableScreens()

const { runSaga, store, persistor } = configureStore()
runSaga(rootSaga)

export default () => (
  <Provider store={store}>
    <PersistGate loading={<Loading />} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
)

Before starting our app we should also set-up routing / navigation. Here is the code we will use (check it out in the repository):

import { createAppContainer, createSwitchNavigator } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'

import CheckAuth from './containers/CheckAuth'
import Login from './containers/Auth/Login'
import CheckConnection from './containers/CheckConnection'
import Users from './containers/Users'
import CallScreen from './containers/CallScreen'
import Info from './containers/Info'
import { navigationHeader } from './theme'

const AppNavigator = createSwitchNavigator({
  CheckAuth,
  Auth: createStackNavigator({
    Login,
    Info,
  }, {
    initialRouteName: 'Login',
    defaultNavigationOptions: navigationHeader,
  }),
  WebRTC: createSwitchNavigator({
    CheckConnection,
    CallScreen,
    Main: createStackNavigator({
      Users,
      Info,
    }, {
      initialRouteName: 'Users',
      defaultNavigationOptions: navigationHeader,
    })
  }, {
    initialRouteName: 'CheckConnection'
  })
}, {
  initialRouteName: 'CheckAuth'
})

export default createAppContainer(AppNavigator)

There is also a logic behind deciding whether we should use StackNavigator or SwitchNavigator. When the application starts it will display a route that will check if the user is authenticated or not. If they are not authenticated, the Login screen will be displayed. Otherwise, we can route the user to the application. But then we should check if we have a connection to chat and connect if not connected. Then we can route the user further. If there is a WebRTC session, route to CallScreen, otherwise to the main screen.

Now that we have navigation, store, and other things set up we can run our app. Let’s update src/App.js to display our router:

export default class Login extends React.Component {

  LOGIN_HINT = 'Use your email or alphanumeric characters in a range from 3 to 50. First character must be a letter.'
  USERNAME_HINT = 'Use alphanumeric characters and spaces in a range from 3 to 20. Cannot contain more than one space in a row.'

  static navigationOptions = ({ navigation }) => ({
    title: 'Enter to videochat',
    headerRight: (
      <HeaderButton
        imageSource={images.INFO}
        onPress={() => navigation.navigate('Info')}
      />
    )
  })

  validate = (values) => {
    const errors = []
    if (values.login) {
      if (values.login.indexOf('@') > -1) {
        if (!emailRegex.test(values.login)) {
          errors.login = this.LOGIN_HINT
        }
      } else {
        if (!/^[a-zA-Z][\w\-\.]{1,48}\w$/.test(values.login)) {
          errors.login = this.LOGIN_HINT
        }
      }
    } else {
      errors.login = this.LOGIN_HINT
    }
    if (values.username) {
      if (!/^(?=.{3,20}$)(?!.*([\s])\1{2})[\w\s]+$/.test(values.username)) {
        errors.username = this.USERNAME_HINT
      }
    } else {
      errors.username = this.USERNAME_HINT
    }
    return errors
  }

  submit = ({ login, username }) => {
    const { createUser, signIn } = this.props
    new Promise((resolve, reject) => {
      signIn({ login, resolve, reject })
    }).then(action => {
      this.checkIfUsernameMatch(username, action.payload.user)
    }).catch(action => {
      const { error } = action
      if (error.toLowerCase().indexOf('unauthorized') > -1) {
        new Promise((resolve, reject) => {
          createUser({
            fullName: username,
            login,
            password: 'quickblox',
            resolve,
            reject,
          })
        }).then(() => {
          this.submit({ login, username })
        }).catch(userCreateAction => {
          const { error } = userCreateAction
          if (error) {
            showError('Failed to create user account', error)
          }
        })
      } else {
        showError('Failed to sign in', error)
      }
    })
  }

  checkIfUsernameMatch = (username, user) => {
    const { updateUser } = this.props
    const update = user.fullName !== username ?
      new Promise((resolve, reject) => updateUser({
        fullName: username,
        login: user.login,
        resolve,
        reject,
      })) :
      Promise.resolve()
    update
      .then(this.connectAndRedirect)
      .catch(action => {
        if (action && action.error) {
          showError('Failed to update user', action.error)
        }
      })
  }

  connectAndRedirect = () => {
    const { connectAndSubscribe, navigation } = this.props
    connectAndSubscribe()
    navigation.navigate('Users')
  }

  renderForm = (formProps) => {
    const { handleSubmit, invalid, pristine, submitError } = formProps
    const { loading } = this.props
    const submitDisabled = pristine || invalid || loading
    const submitStyles = submitDisabled ?
      [styles.submitBtn, styles.submitBtnDisabled] :
      styles.submitBtn
    return (
      <KeyboardAvoidingView
        behavior={Platform.select({ ios: 'padding' })}
        style={styles.topView}
      >
        <ScrollView
          contentContainerStyle={{ alignItems: 'center' }}
          style={styles.scrollView}
        >
          <View style={{ width: '50%' }}>
            <Header>Please enter your login and username</Header>
          </View>
          <View style={styles.formControlView}>
            <Label>Login</Label>
            <Field
              activeStyle={styles.textInputActive}
              autoCapitalize="none"
              blurOnSubmit={false}
              component={FormTextInput}
              editable={!loading}
              name="login"
              onSubmitEditing={() => this.usernameRef.focus()}
              returnKeyType="next"
              style={styles.textInput}
              textContentType="username"
              underlineColorAndroid={colors.transparent}
            />
          </View>
          <View style={styles.formControlView}>
            <Label>Username</Label>
            <Field
              activeStyle={styles.textInputActive}
              autoCapitalize="none"
              component={FormTextInput}
              editable={!loading}
              inputRef={_ref => this.usernameRef = _ref}
              name="username"
              onSubmitEditing={handleSubmit}
              returnKeyType="done"
              style={styles.textInput}
              underlineColorAndroid={colors.transparent}
            />
          </View>
          {submitError ? (
            <Label style={{ alignSelf: 'center', color: colors.error }}>
              {submitError}
            </Label>
          ) : null}
          <TouchableOpacity
            disabled={submitDisabled}
            onPress={handleSubmit}
            style={submitStyles}
          >
            {loading ? (
              <ActivityIndicator color={colors.white} size={20} />
            ) : (
              <Text style={styles.submitBtnText}>Login</Text>
            )}
          </TouchableOpacity>
        </ScrollView>
      </KeyboardAvoidingView>
    )
  }

  render() {
    return (
      <Form
        onSubmit={this.submit}
        render={this.renderForm}
        validate={this.validate}
      />
    )
  }

}

Our application can use QuickBlox React Native SDK for audio/video calls. In order to use this functionality, we need to initialize it. So let’s update src/App.js and add SDK initialization for when the app starts:

import React from 'react'
import { connect } from 'react-redux'
import { StatusBar, StyleSheet, View } from 'react-native'
import FlashMessage from 'react-native-flash-message'

import Navigator from './Navigation'
import NavigationService from './NavigationService'
import { appStart } from './actionCreators'
import { colors } from './theme'
import config from './QBConfig'

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    backgroundColor: colors.primary,
    flex: 1,
    justifyContent: 'center',
    width: '100%',
  },
  navigatorView: {
    flex: 1,
    width: '100%',
  },
})

class App extends React.Component {

  constructor(props) {
    super(props)
    props.appStart(config)
  }

  render() {
    return (
      <View style={styles.container}>
        <StatusBar
          backgroundColor={colors.primary}
          barStyle="light-content"
        />
        <View style={styles.navigatorView}>
          <Navigator ref={NavigationService.init} />
        </View>
        <FlashMessage position="bottom" />
      </View>
    )
  }

}

const mapStateToProps = null

const mapDispatchToProps = { appStart }

export default connect(mapStateToProps, mapDispatchToProps)(App)
</pre>
<p>When <strong>appStart</strong> action creator will fire <strong>APP_START</strong> action, the saga will be triggered (in <strong>src/sagas/app.js</strong>) that will initialize QuickBlox SDK with action payload:</p>
<pre>
export function* appStart(action = {}) {
  const config = action.payload
  try {
    yield call(QB.settings.init, config)
    yield put(appStartSuccess())
  } catch (e) {
    yield put(appStartFail(e.message))
  }
}

Creating the login form

The first thing the user will see in this app will be the login form. Let’s create a component for it:

export default class Login extends React.Component {

  LOGIN_HINT = 'Use your email or alphanumeric characters in a range from 3 to 50. First character must be a letter.'
  USERNAME_HINT = 'Use alphanumeric characters and spaces in a range from 3 to 20. Cannot contain more than one space in a row.'

  static navigationOptions = ({ navigation }) => ({
    title: 'Enter to videochat',
    headerRight: (
      <HeaderButton
        imageSource={images.INFO}
        onPress={() => navigation.navigate('Info')}
      />
    )
  })

  validate = (values) => {
    const errors = []
    if (values.login) {
      if (values.login.indexOf('@') > -1) {
        if (!emailRegex.test(values.login)) {
          errors.login = this.LOGIN_HINT
        }
      } else {
        if (!/^[a-zA-Z][\w\-\.]{1,48}\w$/.test(values.login)) {
          errors.login = this.LOGIN_HINT
        }
      }
    } else {
      errors.login = this.LOGIN_HINT
    }
    if (values.username) {
      if (!/^(?=.{3,20}$)(?!.*([\s])\1{2})[\w\s]+$/.test(values.username)) {
        errors.username = this.USERNAME_HINT
      }
    } else {
      errors.username = this.USERNAME_HINT
    }
    return errors
  }

  submit = ({ login, username }) => {
    const { createUser, signIn } = this.props
    new Promise((resolve, reject) => {
      signIn({ login, resolve, reject })
    }).then(action => {
      this.checkIfUsernameMatch(username, action.payload.user)
    }).catch(action => {
      const { error } = action
      if (error.toLowerCase().indexOf('unauthorized') > -1) {
        new Promise((resolve, reject) => {
          createUser({
            fullName: username,
            login,
            password: 'quickblox',
            resolve,
            reject,
          })
        }).then(() => {
          this.submit({ login, username })
        }).catch(userCreateAction => {
          const { error } = userCreateAction
          if (error) {
            showError('Failed to create user account', error)
          }
        })
      } else {
        showError('Failed to sign in', error)
      }
    })
  }

  checkIfUsernameMatch = (username, user) => {
    const { updateUser } = this.props
    const update = user.fullName !== username ?
      new Promise((resolve, reject) => updateUser({
        fullName: username,
        login: user.login,
        resolve,
        reject,
      })) :
      Promise.resolve()
    update
      .then(this.connectAndRedirect)
      .catch(action => {
        if (action && action.error) {
          showError('Failed to update user', action.error)
        }
      })
  }

  connectAndRedirect = () => {
    const { connectAndSubscribe, navigation } = this.props
    connectAndSubscribe()
    navigation.navigate('Users')
  }

  renderForm = (formProps) => {
    const { handleSubmit, invalid, pristine, submitError } = formProps
    const { loading } = this.props
    const submitDisabled = pristine || invalid || loading
    const submitStyles = submitDisabled ?
      [styles.submitBtn, styles.submitBtnDisabled] :
      styles.submitBtn
    return (
      <KeyboardAvoidingView
        behavior={Platform.select({ ios: 'padding' })}
        style={styles.topView}
      >
        <ScrollView
          contentContainerStyle={{ alignItems: 'center' }}
          style={styles.scrollView}
        >
          <View style={{ width: '50%' }}>
            <Header>Please enter your login and username</Header>
          </View>
          <View style={styles.formControlView}>
            <Label>Login</Label>
            <Field
              activeStyle={styles.textInputActive}
              autoCapitalize="none"
              blurOnSubmit={false}
              component={FormTextInput}
              editable={!loading}
              name="login"
              onSubmitEditing={() => this.usernameRef.focus()}
              returnKeyType="next"
              style={styles.textInput}
              textContentType="username"
              underlineColorAndroid={colors.transparent}
            />
          </View>
          <View style={styles.formControlView}>
            <Label>Username</Label>
            <Field
              activeStyle={styles.textInputActive}
              autoCapitalize="none"
              component={FormTextInput}
              editable={!loading}
              inputRef={_ref => this.usernameRef = _ref}
              name="username"
              onSubmitEditing={handleSubmit}
              returnKeyType="done"
              style={styles.textInput}
              underlineColorAndroid={colors.transparent}
            />
          </View>
          {submitError ? (
            <Label style={{ alignSelf: 'center', color: colors.error }}>
              {submitError}
            </Label>
          ) : null}
          <TouchableOpacity
            disabled={submitDisabled}
            onPress={handleSubmit}
            style={submitStyles}
          >
            {loading ? (
              <ActivityIndicator color={colors.white} size={20} />
            ) : (
              <Text style={styles.submitBtnText}>Login</Text>
            )}
          </TouchableOpacity>
        </ScrollView>
      </KeyboardAvoidingView>
    )
  }

  render() {
    return (
      <Form
        onSubmit={this.submit}
        render={this.renderForm}
        validate={this.validate}
      />
    )
  }

}

This code validates the login and username filled in the form. If login or username does not pass validation, the user will be provided with a hint on how to improve its validity. Once the user’s login and username is approved, the user will be requested to sign-in.

After successful sign-in, the “CHAT_CONNECT_AND_SUBSCRIBE” action is successfully dispatched, which in turn triggers the connectAndSubscribe saga:

export function* connectAndSubscribe() {
  const { user } = yield select(state => state.auth)
  if (!user) return
  const connected = yield call(isChatConnected)
  const loading = yield select(({ chat }) => chat.loading)
  if (!connected && !loading) {
    yield call(chatConnect, {
      payload: {
        userId: user.id,
        password: user.password,
      }
    })
  }
  yield call(setupQBSettings)
  yield put(webrtcInit())
}

Find the source code of these sagas in the repository.

What is the logic behind this code?

Saga checks if there is a user in-store (if the user is authorized). If there is no user – saga ends its execution. If the user is authorized –the connectAndSubscribe saga calls the “isChatConnected” saga. The ConnectAndSubscribe saga then calls the “isConnected” method of the QuickBlox SDK (chat module) to understand if we are connected to chat. The QuickBlox SDK needs to be connected to the chat for audio/video calling because the chat module is used as signalling transport.

If we are not connected to chat and the corresponding flag in store does not indicate that we are trying to connect right now – initiate connecting to chat.

Also, you need to initialize the Webrtc module of QuickBlox React Native SDK. This is an important part if you want to use audio/video calling functionality. Simply call QB.webrtc.init() to get the SDK ready to work with calls.

Then redirect to “Users” route.

Creating a User list

React Native provides several APIs to render lists. To render lists of users we use FlatList:

export default class UsersList extends React.PureComponent {

  componentDidMount() {
    this.props.getUsers()
  }

  loadNextPage = () => {
    const {
      filter,
      getUsers,
      loading,
      page,
      perPage,
      total,
    } = this.props
    const hasMore = page * perPage < total
    if (loading || !hasMore) {
      return
    }
    const query = {
      append: true,
      page: page + 1,
      perPage,
    }
    if (filter && filter.trim().length) {
      query.filter = {
        field: QB.users.USERS_FILTER.FIELD.FULL_NAME,
        operator: QB.users.USERS_FILTER.OPERATOR.IN,
        type: QB.users.USERS_FILTER.TYPE.STRING,
        value: filter
      }
    }
    getUsers(query)
  }

  onUserSelect = (user) => {
    const { selectUser, selected = [] } = this.props
    const index = selected.findIndex(item => item.id === user.id)
    if (index > -1 || selected.length < 3) {
      const username = user.fullName || user.login || user.email
      selectUser({ id: user.id, name: username })
    } else {
      showError(
        'Failed to select user',
        'You can select no more than 3 users'
      )
    }
  }

  renderUser = ({ item }) => {
    const { selected = [] } = this.props
    const userSelected = selected.some(record => record.id === item.id)
    return (
      <User
        isSelected={userSelected}
        onSelect={this.onUserSelect}
        selectable
        user={item}
      />
    )
  }

  renderNoUsers = () => {
    const { filter, loading } = this.props
    if (loading || !filter) {
      return null
    } else return (
      <View style={styles.noUsersView}>
        <Text style={styles.noUsersText}>
          No user with that name
        </Text>
      </View>
    )
  }

  render() {
    const { data, getUsers, loading } = this.props
    return (
      <FlatList
        data={data}
        keyExtractor={({ id }) => `${id}`}
        ListEmptyComponent={this.renderNoUsers}
        onEndReached={this.loadNextPage}
        onEndReachedThreshold={0.85}
        refreshControl={(
          <RefreshControl
            colors={[colors.primary]}
            refreshing={loading}
            tintColor={colors.primary}
            onRefresh={getUsers}
          />
        )}
        renderItem={this.renderUser}
        style={{ backgroundColor: colors.whiteBackground }}
      />
    )
  }

}

When the component will mount, it will dispatch the action that will trigger the users saga which in turn will call QuickBlox SDK to load users:

const defautQuery = {
  sort: {
    ascending: false,
    field: QB.users.USERS_FILTER.FIELD.UPDATED_AT,
    type: QB.users.USERS_FILTER.TYPE.DATE
  }
}
try {
  const query = { ...defaultQuery, ...action.payload }
  const response = yield call(QB.users.getUsers, query)
  yield put(usersGetSuccess(response))
} catch (e) {
  yield put(usersGetFail(e.message))
  showError('Failed to get users', e.message)

Creating a call screen

Call screen should work in case a user initiates a call, but also when the user receives a call request. Also, there can be an audio or video call. Let’s say that for audio calls, we will display circles for opponents (excluding current user) showing the opponent’s name and peer connection status:

const PeerStateText = {
  [QB.webrtc.RTC_PEER_CONNECTION_STATE.NEW]: 'Calling...',
  [QB.webrtc.RTC_PEER_CONNECTION_STATE.CONNECTED]: 'Connected',
  [QB.webrtc.RTC_PEER_CONNECTION_STATE.DISCONNECTED]: 'Disconnected',
  [QB.webrtc.RTC_PEER_CONNECTION_STATE.FAILED]: 'Failed to connect',
  [QB.webrtc.RTC_PEER_CONNECTION_STATE.CLOSED]: 'Connection closed',
}

const getOpponentsCircles = () => {
  const { currentUser, peers, session, users } = this.props
  const userIds = session
    .opponentsIds
    .concat(session.initiatorId)
    .filter(userId => userId !== currentUser.id)
  return (
    <View style={styles.opponentsContainer}>
      {userIds.map(userId => {
        const user = users.find(user => user.id === userId)
        const username = user ?
          (user.fullName || user.login || user.email) :
          ''
        const backgroundColor = user && user.color ?
          user.color :
          colors.primaryDisabled
        const peerState = peers[userId] || 0
        return (
          <View key={userId} style={styles.opponentView}>
            <View style={[styles.circleView, { backgroundColor }]}>
              <Text style={styles.circleText}>
                {username.charAt(0)}
              </Text>
            </View>
            <Text style={styles.usernameText}>
              {username}
            </Text>
            <Text style={styles.statusText}>
              {PeerStateText[peerState]}
            </Text>
          </View>
        )
      })}
    </View>
  )
}

And for the video call, we may show WebRTCView from the QuickBlox React Native SDK:

import WebRTCView from 'quickblox-react-native-sdk/RTCView'

const getVideoViews = () => {
  const { currentUser, opponentsLeftCall, session } = this.props
  const opponentsIds = session ? session
    .opponentsIds
    .filter(id => opponentsLeftCall.indexOf(id) === -1) :
    []
  const videoStyle = opponentsIds.length > 1 ?
    { height: '50%', width: '50%' } :
    { height: '50%', width: '100%' }
  const initiatorVideoStyle = opponentsIds.length > 2 ?
    { height: '50%', width: '50%' } :
    { height: '50%', width: '100%' }
  return (
    <React.Fragment>
      {opponentsIds.map(userId => (
        <WebRTCView
          key={userId}
          mirror={userId === currentUser.id}
          sessionId={session.id}
          style={videoStyle}
          userId={userId}
        />
      ))}
      <WebRTCView
        key={session.initiatorId}
        mirror={session.initiatorId === currentUser.id}
        sessionId={session.id}
        style={initiatorVideoStyle}
        userId={session.initiatorId}
      />
    </React.Fragment>
  )
}

You can find the full code of this component (and container, and other parts) in the repository.

Select users and initiate a call

To start a call, you need to select the user(s) whom to call. It is up to you how to implement the mechanism of user selection. We will focus here on initiating a call when the users are already selected.

...
import QB from 'quickblox-react-native-sdk'
...

const styles = StyleSheet.create({
  ...
})

export default class SelectedUsersAndCallButtons extends React.PureComponent {

  audioCall = () => {
    const { call, users } = this.props
    const opponentsIds = users.map(user => user.id)
    try {
      call({ opponentsIds, type: QB.webrtc.RTC_SESSION_TYPE.AUDIO })
    } catch (e) {
      showError('Error', e.message)
    }
  }

  videoCall = () => {
    const { call, users } = this.props
    const opponentsIds = users.map(user => user.id)
    try {
      call({ opponentsIds, type: QB.webrtc.RTC_SESSION_TYPE.VIDEO })
    } catch (e) {
      showError('Error', e.message)
    }
  }

  render() {
    const { users } = this.props
    return (
      <View style={styles.view}>
        <SelectedUsers users={users} />
        <Button
          disabled={users.length < 1 || users.length > 3}
          onPress={this.audioCall}
          style={styles.button}
        >
          <Image source={CALL} style={styles.icon} />
        </Button>
        <Button
          disabled={users.length < 1 || users.length > 3}
          onPress={this.videoCall}
          style={styles.button}
        >
          <Image source={VIDEO_CALL} style={styles.icon} />
        </Button>
      </View>
    )
  }
 
}

In this example, we are showing separate buttons for audio and video calls which are disabled if there are less than 1 or more than 3 selected users. When pressing the button, the action is dispatched with opponents-IDs (array containing IDs of selected users) and type of call to start. Here is the saga listening for this action:

export function* callSaga(action) {
  try {
    const { payload } = action
    const session = yield call(QB.webrtc.call, payload)
    yield put(webrtcCallSuccess(session))
  } catch (e) {
    yield put(webrtcCallFail(e.message))
  }
}

Once we create a WebRTC session (started a call), we can navigate to a call screen and wait for opponent’s response.

Listening to QuickBlox SDK events

QuickBlox SDK sends events from native code to JS when something happens. In order to receive these events, we should create an emitter from the QuickBlox SDK module. Not every module of the QuickBlox React Native SDK sends events. To find out if a module sends events you can check if that module has EVENT_TYPE property. For example, check the output of following code in the React Native app:

console.log(QB.chat.EVENT_TYPE)
console.log(QB.auth.EVENT_TYPE)
console.log(QB.webrtc.EVENT_TYPE)

Once we create an emitter from the QuickBlox SDK module we can assign an event handler(s) to listen and process events. With redux-saga we can use the eventChannel factory to create a channel for events.

import { NativeEventEmitter } from 'react-native'
import { eventChannel } from 'redux-saga'
import QB from 'quickblox-react-native-sdk'

import {
  CHAT_CONNECT_AND_SUBSCRIBE,
  CHAT_DISCONNECT_REQUEST,
} from '../constants'
import { webrtcReject } from '../actionCreators'
import Navigation from '../NavigationService'

function* createChatConnectionChannel() {
  return eventChannel(emitter => {
    const chatEmitter = new NativeEventEmitter(QB.chat)
    const QBConnectionEvents = [
      QB.chat.EVENT_TYPE.CONNECTED,
      QB.chat.EVENT_TYPE.CONNECTION_CLOSED,
      QB.chat.EVENT_TYPE.CONNECTION_CLOSED_ON_ERROR,
      QB.chat.EVENT_TYPE.RECONNECTION_FAILED,
      QB.chat.EVENT_TYPE.RECONNECTION_SUCCESSFUL,
    ]
    const subscriptions = QBConnectionEvents.map(eventName =>
      chatEmitter.addListener(eventName, emitter)
    )
    return () => {
      while (subscriptions.length) {
        const subscription = subscriptions.pop()
        subscription.remove()
      }
    }
  })
}

function* createWebRTCChannel() {
  return eventChannel(emitter => {
    const webRtcEmitter = new NativeEventEmitter(QB.webrtc)
    const QBWebRTCEvents = Object
      .keys(QB.webrtc.EVENT_TYPE)
      .map(key => QB.webrtc.EVENT_TYPE[key])
    const subscriptions = QBWebRTCEvents.map(eventName =>
      webRtcEmitter.addListener(eventName, emitter)
    )
    return () => {
      while (subscriptions.length) {
        const subscription = subscriptions.pop()
        subscription.remove()
      }
    }
  })
}

function* readConnectionEvents() {
  const channel = yield call(createChatConnectionChannel)
  while (true) {
    try {
      const event = yield take(channel)
      yield put(event)
    } catch (e) {
      yield put({ type: 'ERROR', error: e.message })
    } finally {
      if (yield cancelled()) {
        channel.close()
      }
    }
  }
}

function* readWebRTCEvents() {
  const channel = yield call(createWebRTCChannel)
  while (true) {
    try {
      const event = yield take(channel)
      yield put(event)
    } catch (e) {
      yield put({ type: 'ERROR', error: e.message })
    } finally {
      if (yield cancelled()) {
        channel.close()
      }
    }
  }
}

To start reading events from chatConnection channel and webrtc channel, we can use sagas that will create channels upon login and will close channel(s) upon logout:

function* QBconnectionEventsSaga() {
  try {
    const channel = yield call(createChatConnectionChannel)
    while (true) {
      const event = yield take(channel)
      yield put(event)
    }
  } catch (e) {
    yield put({ type: 'QB_CONNECTION_CHANNEL_ERROR', error: e.message })
  }
}

function* QBWebRTCEventsSaga() {
  try {
    const channel = yield call(createWebRTCChannel)
    while (true) {
      const event = yield take(channel)
      yield call(handleWebRTCEvent, event)
    }
  } catch (e) {
    yield put({ type: 'QB_WEBRTC_CHANNEL_ERROR', error: e.message })
  }
}

You can find full code at the sample repository.

If we want to add a special handler for some specific event(s) we can either put that logic in readWebRTCEvents saga or create a separate event channel for that event(s). Let’s add some more logic into the existing saga:

function* readWebRTCEvents() {
  const channel = yield call(createWebRTCChannel)
  while (true) {
    try {
      const event = yield take(channel)
      // if we received incoming call request
      if (event.type === QB.webrtc.EVENT_TYPE.CALL) {
        const { session, user } = yield select(({ auth, webrtc }) => ({
          session: webrtc.session,
          user: auth.user
        }))
        // if we already have session (either incoming or outgoing call)
        if (session) {
          // received call request is not for the session we have
          if (session.id !== event.payload.session.id) {
            const username = user ?
              (user.fullName || user.login || user.email) :
              'User'
            // reject call request with explanation message
            yield put(webrtcReject({
              sessionId: event.payload.session.id,
              userInfo: { reason: `${username} is busy` }
            }))
          }
        // if we don't have session
        } else {
          // dispatch event that we have call request
          // to add this session in store
          yield put(event)
          // navigate to call screen to provide user with
          // controls to accept or reject call
          Navigation.navigate({ routeName: 'CallScreen' })
        }
      } else {
        yield put(event)
      }
    } catch (e) {
      yield put({ type: 'ERROR', error: e.message })
    } finally {
      if (yield cancelled()) {
        channel.close()
      }
    }
  }
}

Operating call

There isn’t a lot of possible actions we can perform with a call, but let’s go through them to make it clear:

  1. Accept – can be applied to incoming call requests. To accept a call (session) we should specify the session ID we want to accept.
  2. // action creator
    export function acceptCall(payload) {
      return { type: WEBRTC_ACCEPT_REQUEST, payload }
    }
    
    // dispatch action to accept a call
    acceptCall({ sessionId: this.props.session.id })
    
    export function* accept(action) {
      try {
        const { payload } = action
        // call Quickblox SDK to accept a call
        const session = yield call(QB.webrtc.accept, payload)
        // dispatch action to indicate that session accepted successfully
        // session returned is the representation of our active call
        yield put(acceptCallSuccess(session))
      } catch (e) {
        yield put(acceptCallFail(e.message))
      }
    }
    
  3. Reject – can be applied to incoming call requests. To reject (decline) a call request we should specify the session ID we want to reject.
  4. // action creator
    export function rejectCall(payload) {
      return { type: WEBRTC_REJECT_REQUEST, payload }
    }
    
    // dispatch action to reject a call
    rejectCall({ sessionId: this.props.session.id })
    
    export function* reject(action) {
      try {
        const { payload } = action
        // call Quickblox SDK to reject a call
        const session = yield call(QB.webrtc.reject, payload)
        // dispatch action to indicate that session rejected successfully
        yield put(rejectCallSuccess(session))
      } catch (e) {
        yield put(rejectCallFail(e.message))
      }
    }
    
  5. Hang up – end a call. The same as the examples above – we should specify the session ID we want to end.
  6. // action creator
    export function hangUpCall(payload) {
      return { type: WEBRTC_HANGUP_REQUEST, payload }
    }
    
    // dispatch action to end a session (call)
    hangUpCall({ sessionId: this.props.session.id })
    
    export function* hangUp(action) {
      try {
        const { payload } = action
        const session = yield call(QB.webrtc.hangUp, payload)
        // dispatch an action to indicate that hung up successfully
        yield put(hangUpCallSuccess(session))
      } catch (e) {
        yield put(hangUpCallFail(e.message))
      }
    }
    

    You’re all set!

    Once you have completed all the steps of this guide, you will have a fully-functional React Native chat app. Our React Native SDK is the perfect cross-platform video call SDK for Android and iOS platform-based apps.

    Have questions or suggestions about our video call SDK and documentation? Please contact our support team and don’t forget to check out our React Native documentation & code samples.

    Frequently asked questions

    Q: I want to accept a call. Where can I get a session ID to pass in QB.webrtc.accept?

    A: When you create a session or receive a call request (event), the QuickBlox SDK returns session information including ID, session type (audio/video) and other information.

    Q: I am initiating a call but my opponent does not seem to be online and is not accepting, or rejecting the call. How long will my device be sending a call request?

    A: If there is no answer to call request within 60 seconds, the QuickBlox SDK will close the session and send you the event “@QB/NOT_ANSWER” (QB.webrtc.EVENT_TYPE.NOT_ANSWER), meaning that there was no answer from the opponent(s). You can configure how long to wait for an answer as described in our video calling documentation.

    Have Questions? Need Support?

    Join the QuickBlox Developer Discord Community, where you can share ideas, learn about our software, & get support.

    Join QuickBlox Discord
  1. Royal says:

    Ꮋello thегe! Dо you uѕe Twitter? Ӏ’d liқe to follow you if that would Ьe ok.
    I’m absolutely enjoying your blog and
    look forward tо neѡ updates.

    Thank Royal. Please follow us on Twitter: https://twitter.com/quickblox

Leave a Comment

Your email address will not be published. Required fields are marked *

Read More

Ready to get started?

QUICKBLOX
QuickBlox post-box