Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

How to Build a Video Calling App With a Call Ringer

In today's world, video calling has become an essential tool for communication, especially since the COVID-19 pandemic. With the increasing growing demand for video conferencing solutions, developers are constantly looking for ways to integrate video calling features into their applications.

A key issue when dealing with video SDKs is the absence of a continuous user notification system, similar to the ringing feature seen in apps like WhatsApp and FaceTime. This functionality is vital for developers to replicate the real-time, familiar video call experience.

In this blog post, we will explore how to build a robust video call app with a call notification like Facetime using Dyte Video SDK and React Native.

Prerequisite

  • A Dyte developer account, which you can create for free at dev.dyte.io.
  • Basic understanding of Typescript, React Native, and Node js.
  • Conceptual knowledge of asynchronous communication and cloud messaging.
  • Any IDE that supports Android projects and Typescript: Webstorm by JetBrains, vs-code, etc.

Techstack

  • React Native (Typescript) for the Android app
  • NativeWind for styling
  • Node.js for a simple HTTP server
  • React Native Firebase for cloud messaging

Step 0: Setup and configuration

First, let's ensure we have all the tools and programs required for running a React Native App on our system.

Setting up React Native

To develop React Native applications, we must have Android SDK on our system. This typically involves installing LTS versions of Node.js, Java SE Development Kit (JDK), and Android SDK. This is covered in detail in React Native’s official documentation.

Creating a Dyte developer account

We’ll need a free account to access Dyte’s APIs for creating and managing meetings. Head over to dev.dyte.io and register for a free account. Once you log in, head over to the developer dashboard > API Keys and copy the Organization ID and API Key from the dashboard.

Creating a Firebase project

To enable cloud messaging in our application, we need to create a new project on Firebase. Go to console.firebase.google.com, log in with your Google account, and create a new project. Enter your project’s name, follow the instructions, and complete the project setup.

Now we’re ready to work on our application.

Step 1: Create a React Native app and install dependencies

According to the official documentation, we can create a React Native app using either the CLI or through Expo. In this blog, we’ll use React Native CLI to create our Android app as follows:

npx react-native init react-native-video-call

This will create our app using Typescript and Yarn for package management. Next, install node_modules from package.json using:

yarn install

To run the application, connect your Android device to your machine via USB and make sure USB debugging is enabled from the developer settings. Once the connection is established, run the app using with:

npx react-native run-android

This will launch the sample boilerplate app on your Android device. Next, follow the official docs to set up and configure NativeWind in the project.

NativeWind is a great utility for styling React Native applications. NativeWind uses Tailwind CSS as a scripting language to create a universal style system for React Native. We can style React Native components using Taillwind’s classes via the className prop.

Make sure to check the typescript config section for troubleshooting typescript errors!

Next, we need to install some third-party libraries in the project:

  • @dytesdk/react-native-core - for using Dyte’s video call core services
  • @dytesdk/react-native-ui-kit - for using Dyte’s video call meeting interface
  • @react-native-firebase/app - for connecting with our firebase project
  • @react-native-firebase/messaging - for firebase cloud messaging services
  • react-native-callkeep - for using Android’s native Caller UI
  • zustand - un-opinionated state-management solution for managing global app state

Install the above dependencies one by one or through a single command as follows:

yarn add @dytesdk/react-native-core @dytesdk/react-native-ui-kit  @react-native-firebase/app @react-native-firebase/messaging react-native-callkeep zustand

Install the required dependencies for Dyte’s SDK using the following command:

yarn add react-native-webrtc react-native-document-picker react-native-file-viewer react-native-fs react-native-safe-area-context react-native-sound-player react-native-svg react-native-webview

Apart from these, we’ll also need React navigation for navigating between screens in our applications, and axios for making HTTP requests:

yarn add react-native-screens react-native-safe-area-context axios 

Check their documentation for further configuration and troubleshooting.

Step 2: Setting up an HTTP server

The HTTP server will act as a mediator between our React Native app and the Firebase cloud messaging service. This will allow us to manage the incoming messages from the app and adequately route them to their destination.

First, set up a node.js project with:

yarn init -y

Add dependencies:

yarn add express body-parser firebase-admin

We will use firebase-admin to communicate with the FCM service from our server. More info here.

To enable device-to-device communication using Firebase, we can either generate a device token for each device, which is then saved in the server for future use. Or we can avoid the hassle of managing tokens by using topics.

Devices can subscribe to a topic, the server can push any incoming message to the relevant topic, and all the subscribed devices will receive the new message sent by the server. The code for this is as follows:

const express = require('express')
const bodyParser = require('body-parser')
const admin = require("firebase-admin");
const serviceAccount = require("./serviceAccountKey.json");

const app = express()
app.use(bodyParser.json());

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: ""
});

app.post("/onInitMeeting", (req, res) => {
  const {meeting, contact, caller} = req.body;
  const data = JSON.stringify({meeting, contact, caller});

  const message = {
    data: {
      payload: data
    },
    topic: 'activeMeeting',
  };

  admin
    .messaging()
    .send(message)
    .then(response => {
      console.log('Successfully sent message:', response);
      res.json({
        success: true
      });
    })
    .catch(error => {
      console.log('Error sending message:', error);
      res.json({
        success: false
      });
    });
})

app.listen(process.env.PORT || 3000)

To use the FCM service on the server, we will need Firebase credentials. This is a JSON file that can be downloaded from this link:

That’s it for the server.

Step 3: Working on the application

First, we need to configure our React Native app to use react-native-firebase. Follow the official docs to set up react-native-firebase for Android and iOS. Next, we need to subscribe to the topic that will receive meeting information from our HTTP server:

import messaging from '@react-native-firebase/messaging';
.
.
function App() {
  .
  .
  .
  useEffect(() => {
    messaging()
      .subscribeToTopic('activeMeeting')
      .then(() => console.log('Subscribed to topic!'));
  }, []);

  return (...)
}
.
.
.

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/App.tsx

Now, our React Native app will intercept all messages that are sent to the specified topic by the server. Our app will have 3 screens: One register-now screen, one contacts screen, and a join-call screen that manages the active meeting.

Registration screen


This is the entry point to our application. The user has to provide a unique username and a display name for using the application:

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/src/pages/Register/index.tsx

The continue button will then redirect the user to the contacts page. 

My contacts

This will hold a list of contacts with whom we can start a video call. The user can tap on any contact to start a video call with them.

To start a call with the selected contact, we simply pass the selected contact to the join-call screen, which will handle the meeting connection:

function initiateCall(contact: Contact) { 
  navigation.push('join-call', { contact });
}

We will also listen for any new messages from the FCM service on this screen. We can register a message listener in the useEffect hook as follows:

useEffect(() => {
    return messaging().onMessage(onPayload);
  }, [onPayload]);

The onMessage function accepts a callback function that is fired whenever a new message is received from the FCM service:

const onPayload = useCallback(
    (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
      if (!remoteMessage.data) {
        console.log('empty data');
        return;
      }

      const payload = JSON.parse(
        remoteMessage.data.payload,
      ) as FirebaseSnapshot;
      const {meeting, contact, caller} = payload;

      log('payload: ', payload);

      if (contact.username === username) {        
        setDetails({meeting, caller});
        RNCallKeep.displayIncomingCall(uuidv4(), caller.username, caller.name);
      } else {
        console.log('BAD USERNAME');
      }
    },
    [username],
  );

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/src/pages/ContactList/index.tsx

Here we first check if the message is valid and has a payload. Then we initiate the “incoming call” caller ui using the react-native-callkeep library. This is how it looks in action:

As you might have guessed, to make this work, we need to keep this screen open, i.e., the incoming call will not be intercepted if we exit the app. We will fix this using a Headless JS task in the next section.

The meeting screen

This screen is where we will manage meetings using Dyte’s SDK. Creating meetings with Dyte is really simple and easy. See the following tsx snippet:

// Add these imports and implement useDyteClient hook
import {DyteProvider, useDyteClient} from '@dytesdk/react-native-core';
import {DyteMeeting, DyteUIProvider} from '@dytesdk/react-native-ui-kit';
const [client, initClient] = useDyteClient();

// Return this component for Meeting Screen

      Meeting with {activeMeeting?.id ? caller?.name : contact.name}
    
    {participant?.token && meeting?.title ? (
     
    ) : (
      Loading...
    )}
  ;

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/src/pages/JoinCall/index.tsx

Believe it or not, this is all it takes to create fully functional meetings using Dyte’s SDK. We only need to initialize the DyteMeeting component with the clientId, roomName, and authToken. Handling audio and video, managing meeting participants, etc, all functionalities are provided by this component out-of-the-box!!

To start a video call with the target contact, we have to follow the following steps:

  • Create a new meeting
  • Add the current user to that meeting
  • Simultaneously, we notify the server that a new meeting has started, so that the target device can catch the call details

We can create a meeting as follows:

function _createMeeting() {
  return new Promise((resolve, reject) => {
    axios
      .post(
        'https://api.cluster.dyte.in/v2/meetings', {
          title: 'Meeting-' + Math.random().toString(8),
        }, {
          headers: {
            Authorization: `Basic ${basic_token}`,
          },
        },
      )
      .then(({ data }) => {
        if (data.success) {
          resolve(data.data);
        }
        reject('failed');
      })
      .catch(err => {
        log('cannot create meeting', err);
        reject('failed');
      });
  });
}

On success, this will return meeting details, which we can store for future use:

_createMeeting().then(meetInfo => {
  log('meeting info:-', meetInfo);
  setMeeting(meetInfo);
});

Next, to add new participants to the meeting:

function _addParticipant(meetId: string, username: string, fullName: string) {
  return new Promise((resolve, reject) => {
    axios
      .post(
        `https://api.cluster.dyte.in/v2/meetings/${meetId}/participants`,
        {
          name: fullName,
          preset_name: "group_call_host",
          custom_participant_id: username,
        },
        {
          headers: {
            Authorization: `Basic ${basic_token}`,
            "Content-Type": "application/json",
          },
        }
      )
      .then(({ data }) => {
        if (data.success) {
          resolve(data.data);
        }
        reject("failed");
      })
      .catch((_) => {
        reject("failed");
      });
  });
}

On success, this will return the new participants’ info. This information contains the authToken, which is a required prop for the DyteMeeting component we saw earlier, and then we initialize the DyteComponent by passing the:

addParticipant(meeting.id, localUsername, localFullName).then(
  (participantInfo) => {
    log("participant info:-", participantInfo);
    if (participantInfo?.token) {
      initClient({
        authToken: participantInfo?.token,
        defaults: {
         audio: true,
         video: true,
        },
      });
    }
    setParticipant(participantInfo);
  }
);

As mentioned in the docs, the DyteMeeting component provides a onInit prop, which expects a callback function that is fired when the meeting is started. This is where we notify our HTTP server about the new meeting:

function onInitMeeting (_meeting: Meeting) => {
  log('received meeting:-', _meeting);
  if (!meeting?.id) {
    return;
  }
  console.log('init');
  if (!client) {
    return;
  }
  client.self.on('roomLeft', () => {
     setParticipant(undefined);
     navigation.goBack();
   });
  notifyServer({
    meeting,
    contact,
    caller: {username: localUsername, name: localFullName, icon: ''},
   });
 };

The notifyServer function is a regular POST request that sends meeting info, target contact, and caller details to the server:

export function notifyServer(payload: {
  meeting?: Meeting;
  contact: Contact;
  caller: Contact;
}) {
  axios.post(server_url + "/onInitMeeting", payload).then(({ data }) => {
    console.log("sent to " + server_url + "/onInitMeeting");
    console.log(data);
  });
}

With that, the meeting screen is ready to use. Below is a demo from the caller’s point of view:

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/src/pages/JoinCall/index.tsx

Step 4: Receiving calls when the app isn’t active

Currently, the app will receive incoming calls only when the app is active, i.e., it is in the foreground state. If we quit the application, then we won’t receive any incoming calls, which isn’t practical for a video calling application.

To fix this, we can use the Headless JS API from React Native. Headless JS is a way to run tasks in JavaScript while your app is in the background. It can be used, for example, to sync fresh data, handle push notifications, play music, or in our case, listen for incoming calls from the background.

To do this, we first need to register a background event listener for react-native-firebase as follows:

useEffect(() => {
  return messaging().setBackgroundMessageHandler(async (remoteMessage) => {
    onPayload(remoteMessage);
    return Promise.resolve();
  });
}, [onPayload]);

As mentioned in their documentation, when the application is in a background or quit state, the onMessage handler will not be called when receiving messages. Instead, we need to set up a background callback handler via the setBackgroundMessageHandler method.

This will enable us to process the incoming messages from the FCM service, but our app is still in the background. We need to bring the app back to the foreground once the call is accepted. To accomplish this, we modify the OnAnswerCallAction as follows:

function onAnswerCallAction(data: { callUUID: string }) {
  const { callUUID } = data;
  RNCallKeep.endCall(callUUID);
  reOpenApp();

  if (details) {
    log("details", details);
    navigation.push("join-call", {
      contact: { name: "", username: "", icon: "" },
      caller: details.caller,
      activeMeeting: details.meeting,
    });
  } else {
    console.log("DETAILS NULL");
  }
}

Notice the use of reOpenApp function above. This is the Headless JS task that will bring our app back to the foreground:

export const reOpenApp = async () => {
  console.log("opening ringer://");
  return Linking.openURL("ringer://open");
};

Here we basically make use of the Linking interface from React Native. Linking provides us with a general interface to interact with both incoming and outgoing app links. In order to bring our app back to the foreground, we will define a custom URL scheme for our app:

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/android/app/src/main/AndroidManifest.xml

This will trigger our app whenever we try to open the URLs of the form ringer://open. So overall, we check for incoming calls using the background listener, then once we get the call notification, we will force the app to open a URL that matches the URL scheme for our application, so that our app can come back to the foreground from the quit state. This is known as Deep linking.

Lastly, we need to register our reOpenApp function as a Headless JS task as follows:

import { AppRegistry } from "react-native";
.
.
.
AppRegistry.registerHeadlessTask("reopen app", reOpenApp);

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/index.js

This will allow us to execute code from the background state.

Thus, the app can now receive call notifications from the background. Below is a quick demo for the same:

Source: https://github.com/dyte-io/video-calling-app-with-ringer/react-native-video-call/blob/master/src/pages/ContactList/index.tsx

With that, our app is complete!! The complete source code of this demo is kept at - https://github.com/dyte-io/video-calling-app-with-ringer/.

Conclusion

That’s how easy it is to create a live video call app with Dyte!!

Building a video call app with Dyte Video SDK and React Native offers a powerful and efficient solution for incorporating real-time communication into your mobile applications.

By harnessing the capabilities of Dyte SDK and the flexibility of React Native, developers can create seamless and high-quality video calling experiences for their users.

So why wait? Start building your own video call app today and unlock a world of possibilities in real-time communication with Dyte! 🚀




This post first appeared on Dyte, please read the originial post: here

Share the post

How to Build a Video Calling App With a Call Ringer

×

Subscribe to Dyte

Get updates delivered right to your inbox!

Thank you for your subscription

×