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.
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:
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
We will use separate folders for components, containers, sagas, and other parts of the application:
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: '', }
Our application has several points that we should configure. Let’s go over them:
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)) } }
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.
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)
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.
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.
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() } } } }
There isn’t a lot of possible actions we can perform with a call, but let’s go through them to make it clear:
// 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)) } }
// 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)) } }
// 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)) } }
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.
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.
Ꮋ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