DevGang
Авторизоваться

Как создать нативное приложение для видеозвонков React с Callkeep, используя Firebase и Video SDK

В мире, где мы все связаны через телефоны с помощью аудио- и видеозвонков, если вы планируете создать одно такое приложение, вы попали в нужное место.

Мы будем создавать полноценное приложение для видеозвонков в React Native, которое позволит вам беспрепятственно совершать и принимать видеозвонки. Мы будем использовать VideoSDK для видеоконференций и React Native CallKeep для управления пользовательским интерфейсом вызовов. Это серия из двух частей, в которых мы сначала реализуем CallKeep для Android, а затем настроим и настроим его для iOS.

Теперь, когда все требования хорошо объяснены, давайте перейдем непосредственно к самой интересной части, но если вам не терпится увидеть результаты, вот ссылка на тестирование приложения и полный код для приложения.

CallKeep

CallKeep - это встроенная библиотека React, которая позволяет вам обрабатывать пользовательский интерфейс входящего вызова на устройствах Android и iOS в любом заданном состоянии приложения, т.е. на переднем плане (запущено), в фоновом режиме, при выходе, заблокированном устройстве и т.д.

Перед созданием приложения вы должны быть осведомлены о том, как приложение будет функционировать внутри, что, в свою очередь, облегчит процесс разработки.

Работа приложения

Чтобы лучше понять, как работает приложение, давайте рассмотрим сценарий, в котором Джон хочет позвонить своему другу Максу. Джон начнет с открытия нашего приложения, где он введет идентификатор вызывающего абонента Макса и нажмет вызов. Макс увидит пользовательский интерфейс входящего вызова на своем телефоне, где он может принять или отклонить вызов. Как только он примет вызов, мы настроим видеозвонок React Native между ними с помощью Video SDK.

Вы можете подумать, что это очень просто. Что ж, давайте подробнее остановимся на нюансах реализации.

  1. Когда Джон вводит идентификатор вызывающего абонента Макса и нажимает кнопку вызова, первое, что мы делаем, это сопоставляем его с нашей базой данных Firebase и отправляем уведомление на его устройство.
  2. Когда устройство Макса получит эти уведомления, логика вашего приложения покажет ему пользовательский интерфейс входящего вызова с использованием библиотеки React Native CallKeep.
  3. Когда Макс примет или отклонит входящий вызов, мы отправим статус обратно Джону с помощью уведомлений и, в конечном итоге, начнем видеозвонок между ними.
  4. Вот наглядное представление потока для лучшего понимания.
Вот графическое представление потока для лучшего понимания.
Вот графическое представление потока для лучшего понимания.

Теперь, когда мы выяснили, как работает приложение и как оно функционирует, давайте приступим к разработке без лишней болтовни.

Требования и библиотеки для приложения

Во-первых, давайте взглянем на набор библиотек, которые мы будем использовать для настройки функциональных возможностей приложения.

  1. React Native CallKeep: эти библиотеки помогут с вызовом входящего вызова на устройстве.
  2. React Native VoIP Push Notification: Эти библиотеки используются для отправки push-уведомлений на устройствах iOS, поскольку уведомления Firebase плохо работают на устройствах iOS, когда приложение находится в нерабочем состоянии.
  3. VideoSDK RN Android Overlay Permission: Эти библиотеки будут обрабатывать разрешение наложения для более новых версий Android, гарантируя, что входящий вызов всегда будет виден.
  4. React Native Firebase Messaging: Эти библиотеки используются для отправки и получения уведомления Firebase, которое вызовет наш пользовательский интерфейс входящего вызова.
  5. React Native Firebase Firestore: Эти библиотеки используются для хранения идентификатора вызывающего абонента и токена устройства, которые будут использоваться для установления видеозвонков.

Если мы посмотрим на требования к разработке, то вот что вам понадобится:

  • Node.js v12+
  • NPM v6+ (входит в состав более новых версий Node)
  • Android Studio и Xcode установлены.
  • Маркер Video SDK (Dashboard > Api-Key) (видеоруководство)
  • Для тестирования функции вызова требуется минимум два физических устройства.

Настройка React Native Android

Давайте начнем с создания нового приложения react native с помощью команды:

npx react-native init VideoSdkCallKeepExample

Теперь, когда наше базовое приложение создано, давайте начнем с установки всех зависимостей.

  • Сначала мы установим @react-navigation/native и другие его зависимости, чтобы обеспечить навигацию внутри приложения.
npm install @react-navigation/native
npm install @react-navigation/stack
npm install react-native-screens react-native-safe-area-context react-native-gesture-handler
  • Вторым в нашем списке зависимостей является библиотека Video SDK, которая обеспечит видеоконференцсвязь с приложением.
npm install "@videosdk.live/react-native-sdk"
npm install "@videosdk.live/react-native-incallmanager"
  • Далее будет установка зависимостей, связанных с Firebase.
npm install @react-native-firebase/app
npm install @react-native-firebase/messaging
npm install @react-native-firebase/firestore
npm install firebase
  • И, наконец, библиотека React Native CallKeep и другие библиотеки, необходимые для push-уведомлений и разрешений.
npm install git+https://github.com/react-native-webrtc/react-native-callkeep#4b1fa98a685f6502d151875138b7c81baf1ec680
npm install react-native-voip-push-notification
npm install videosdk-rn-android-overlay-permission
npm install react-native-uuid
Примечание. Мы поместили ссылку на библиотеку React Native CallKeep, используя ссылку на репозиторий github, поскольку версия NPM имеет проблемы со сборкой с android.

Мы все настроены на наши зависимости. Давайте теперь начнем с настройки Android для всех установленных нами библиотек.

Настройка VideoSDK

Начнем с добавления необходимых разрешений и метаданных в файл AndroidManifest.xml. Ниже перечислены все разрешения, которые необходимо добавить в android/app/src/mainAndroidManifest.xml.

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Needed to communicate with already-paired Bluetooth devices. (Legacy up to Android 11) -->
<uses-permission
                 android:name="android.permission.BLUETOOTH"
                 android:maxSdkVersion="30" />
<uses-permission
                 android:name="android.permission.BLUETOOTH_ADMIN"
                 android:maxSdkVersion="30" />

<!-- Needed to communicate with already-paired Bluetooth devices. (Android 12 upwards)-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Needed to access Camera and Audio -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION" /> 
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />  

<application>
    // ...
    <meta-data android:name="live.videosdk.rnfgservice.notification_channel_name"
      android:value="Meeting Notification"
     />
    <meta-data android:name="live.videosdk.rnfgservice.notification_channel_description"
    android:value="Whenever meeting started notification will appear."
    />
    <meta-data
    android:name="live.videosdk.rnfgservice.notification_color"
    android:resource="@color/red"
    />
    <service android:name="live.videosdk.rnfgservice.ForegroundService" android:foregroundServiceType="mediaProjection"></service>
    <service android:name="live.videosdk.rnfgservice.ForegroundServiceTask"></service>
    // ...
</application>

Добавьте следующие строки в файл build.gradle уровня приложения в android/app/build.gradle внутри dependencies {}

implementation project(':rnfgservice')
implementation project(':rnwebrtc')
implementation project(':rnincallmanager')

Добавьте следующие строки в файл android/settings.gradle.

include ':rnwebrtc'
project(':rnwebrtc').projectDir = new File(rootProject.projectDir, '../node_modules/@videosdk.live/react-native-webrtc/android')

include ':rnincallmanager'
project(':rnincallmanager').projectDir = new File(rootProject.projectDir, '../node_modules/@videosdk.live/react-native-incallmanager/android')

include ':rnfgservice'
project(':rnfgservice').projectDir = new File(rootProject.projectDir, '../node_modules/@videosdk.live/react-native-foreground-service/android')

Обновите MainApplication.java со следующими пакетами.

//Add these imports
import live.videosdk.rnfgservice.ForegroundServicePackage; 
import live.videosdk.rnincallmanager.InCallManagerPackage;
import live.videosdk.rnwebrtc.WebRTCModulePackage;

public class MainApplication extends Application implements ReactApplication {
  private static List<ReactPackage> getPackages() {
    @SuppressWarnings("UnnecessaryLocalVariable")
    List<ReactPackage> packages = new PackageList(this).getPackages();
    // Packages that cannot be autolinked yet can be added manually here, for example:
    // packages.add(new MyReactNativePackage());

    //Add these packages
    packages.add(new ForegroundServicePackage());
    packages.add(new InCallManagerPackage());
    packages.add(new WebRTCModulePackage());
    return packages;
  }
}

Наконец, зарегистрируйте сервис VideoSDK в приложении в index.js файл.

// Import the library
import { register } from '@videosdk.live/react-native-sdk';

// Register the VideoSDK service
register();

Настройка CallKeep для приложения React Native для Android

Начнем с добавления необходимых разрешений и метаданных в файл AndroidManifest.xml. Ниже перечислены все разрешения, которые необходимо добавить в android/app/src/mainAndroidManifest.xml.

<!-- Needed to for the call trigger purpose -->
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />

<application>
    // ...

    <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true"
        >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>


        //...Add these intent filter to allow deep linking
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="videocalling" />
          </intent-filter>
      </activity>

    <service android:name="io.wazo.callkeep.VoiceConnectionService"
        android:label="Wazo"
        android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
        android:foregroundServiceType="camera|microphone"
        android:exported:"true"
    >

        <intent-filter>
            <action android:name="android.telecom.ConnectionService" />
        </intent-filter>
    </service>
    <service android:name="io.wazo.callkeep.RNCallKeepBackgroundMessagingService" />
    // ....
</application>

Настройка Firebase для приложения React Native для Android

Для начала перейдите и создайте новый проект Firebase отсюда.

Как только проект будет создан, добавьте свое приложение react native для Android в проект firebase, нажав на значок Android.

Добавьте идентификатор вашего приложения в представленные поля и нажмите «Register App».

Загрузите файл google-services.json и переместите его в папку android/app.

Следуйте инструкциям, показанным для добавления Firebase SDK в приложение для Android.

Создайте новое веб-приложение в своем проекте firebase, которое будет использоваться для доступа к базе данных firestore.

Добавьте файл конфигурации, показанный в файле database/firebaseDb.js, в свой проект.

Перейдите в Firebase Firestore на левой панели и создайте базу данных, которую мы будем использовать для хранения идентификаторов вызывающих абонентов.

С этим у нас все готово к firebase на Android.

Настройка на стороне сервера

Теперь, когда мы завершили настройку нашего приложения. Давайте также настроим API на стороне сервера. Для создания этих API мы будем использовать функции Firebase. Так что давайте сразу перейдем к делу.

Перейдите к функциям Firebase на левой панели. Чтобы использовать функции Firebase, вам нужно будет перейти на тарифный план "оплата по мере поступления". Хотя нет необходимости беспокоиться о сборах, если вы просто строите в качестве хобби-проекта, поскольку существует щедрая бесплатная квота.

Давайте начнем с функциями Firebase, установив интерфейс командной строки firebase с помощью приведенной ниже команды.

npm install -g firebase-tools

Запустите firebase login, чтобы войти в систему через браузер и аутентифицировать интерфейс Firebase CLI.

Перейдите в каталог вашего проекта Firebase.

Запустите firebase init functions, чтобы инициализировать проект функций Firebase, в котором мы будем писать наши API. Следуйте инструкциям по установке, приведенным в интерфейсе командной строки, и как только процесс завершится, вы увидите папку functions, созданную в вашем каталоге.

Загрузите ключ сервисной учетной записи из настроек проекта и поместите его в файл functions/serviceAccountKey.json.

На этом мы завершили настройку, необходимую для запуска нашего приложения.

Код на стороне приложения

Давайте перейдем к запуску кода на стороне react native. Мы создадим два экрана, первый будет таким, где пользователь сможет увидеть свой идентификатор вызывающего абонента и ввести идентификатор вызывающего абонента другого человека, чтобы инициировать новый вызов.

Мы будем следовать приведенной ниже структуре папок:

.
└── Root/
    ├── android
    ├── ios
    ├── src/
    │ ├── api/
    │ │ └── api.js
    │ ├── assets/
    │ │ └── Get it from our repository
    │ ├── components/
    │ │ ├── Get it from our repository
    │ ├── navigators/
    │ │ └── screenNames.js
    │ ├── scenes/
    │ │ ├── home/
    │ │ │ └── index.js
    │ │ └── meeting/
    │ │ ├── OneToOne/
    │ │ ├── index.js
    │ │ └── MeetingContainer.js
    │ ├── styles/
    │ │ ├── Get it from our repository
    │ └── utils/
    │ └── incoming-video-call.js
    ├── App.js
    ├── index.js
    └── package.json

Давайте начнем с базового UI экрана инициирования вызова.

Чтобы дать вам фору, мы уже создали основные компоненты, которые нам понадобятся, такие как кнопки, текстовые поля, аватары и иконки. Вы можете напрямую получить доступ ко всем значкам и компонентам из нашего репозитория github.

После настройки наших базовых компонентов давайте добавим в приложение навигационные экраны. У нас будет домашний экран, на котором будет ввод идентификатора вызывающего абонента и кнопка вызова, а также экран встречи, на котором будет видеовызов.

Поэтому обновите src/navigators/screenNames.js, указав следующие имена экранов.

export const SCREEN_NAMES = {
  Home: "homescreen",
  Meeting: "meetingscreen",
};

Обновите файл App.js с помощью стека навигации.

import React, { useEffect } from "react";
import "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { SCREEN_NAMES } from "./src/navigators/screenNames";
import Meeting from "./src/scenes/meeting";
import { LogBox, Text, Alert } from "react-native";
import Home from "./src/scenes/home";
import RNCallKeep from "react-native-callkeep";
LogBox.ignoreLogs(["Warning: ..."]);
LogBox.ignoreAllLogs();

const { Navigator, Screen } = createStackNavigator();

const linking = {
  prefixes: ["videocalling://"],
  config: {
    screens: {
      meetingscreen: {
        path: `meetingscreen/:token/:meetingId`,
      },
    },
  },
};

export default function App() {

  return (
    <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
      <Navigator
        screenOptions={{
          animationEnabled: false,
          presentation: "modal",
        }}
        initialRouteName={SCREEN_NAMES.Home}
      >
        <Screen
          name={SCREEN_NAMES.Meeting}
          component={Meeting}
          options={{ headerShown: false }}
        />
        <Screen
          name={SCREEN_NAMES.Home}
          component={Home}
          options={{ headerShown: false }}
        />
      </Navigator>
    </NavigationContainer>
  );
}

Когда наш навигационный стек готов, давайте настроим UI главного экрана.

Для чего вам нужно обновить src/scenes/home/index.js

import React, { useEffect, useState, useRef } from "react";
import {
  Platform, KeyboardAvoidingView, TouchableWithoutFeedback,
  Keyboard, View, Text, Clipboard, Alert, Linking,
} from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { CallEnd, Copy } from "../../assets/icons";
import TextInputContainer from "../../components/TextInputContainer";
import colors from "../../styles/colors";
import firestore from "@react-native-firebase/firestore";
import messaging from "@react-native-firebase/messaging";
import Toast from "react-native-simple-toast";
import {
  updateCallStatus, initiateCall,
  getToken, createMeeting,
} from "../../api/api";
import { SCREEN_NAMES } from "../../navigators/screenNames";
import Incomingvideocall from "../../utils/incoming-video-call";

export default function Home({ navigation }) {

  //These is the number user will enter to make a call
  const [number, setNumber] = useState("");

  //These will store the detials of the users callerId and fcm token
  const [firebaseUserConfig, setfirebaseUserConfig] = useState(null);

  //Used to render the UI conditionally, whether the person on making a call or not
  const [isCalling, setisCalling] = useState(false);

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === "ios" ? "padding" : "height"}
      style={{
        flex: 1,
        backgroundColor: colors.primary["900"],
        justifyContent: "center",
        paddingHorizontal: 42,
      }}
    >
      {!isCalling ? (
        <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
          <>
            <View
              style={{
                padding: 35,
                backgroundColor: "#1A1C22",
                justifyContent: "center",
                alignItems: "center",
                borderRadius: 14,
              }}
            >
              <Text
                style={{
                  fontSize: 18,
                  color: "#D0D4DD",
                }}
              >
                Your Caller ID
              </Text>
              <View
                style={{
                  flexDirection: "row",
                  marginTop: 12,
                  alignItems: "center",
                }}
              >
                <Text
                  style={{
                    fontSize: 32,
                    color: "#ffff",
                    letterSpacing: 8,
                  }}
                >
                  {firebaseUserConfig
                    ? firebaseUserConfig.callerId
                    : "Loading.."}
                </Text>
                <TouchableOpacity
                  style={{
                    height: 30,
                    aspectRatio: 1,
                    backgroundColor: "#2B3034",
                    marginLeft: 12,
                    justifyContent: "center",
                    alignItems: "center",
                    borderRadius: 4,
                  }}
                  onPress={() => {
                    Clipboard.setString(
                      firebaseUserConfig && firebaseUserConfig.callerId
                    );
                    if (Platform.OS === "android") {
                      Toast.show("Copied");
                      Alert.alert(
                        "Information",
                        "This callerId will be unavailable, once you uninstall the App."
                      );
                    }
                  }}
                >
                  <Copy fill={colors.primary[100]} width={16} height={16} />
                </TouchableOpacity>
              </View>
            </View>

            <View
              style={{
                backgroundColor: "#1A1C22",
                padding: 40,
                marginTop: 25,
                justifyContent: "center",
                borderRadius: 14,
              }}
            >
              <Text
                style={{
                  fontSize: 18,
                  color: "#D0D4DD",
                }}
              >
                Enter call id of another user
              </Text>
              <TextInputContainer
                placeholder={"Enter Caller ID"}
                value={number}
                setValue={setNumber}
                keyboardType={"number-pad"}
              />
              <TouchableOpacity
                onPress={async () => {
                  if (number) {

                    //1. getCallee is used to get the detials of the user you are trying to intiate a call with
                    const data = await getCallee(number);
                    if (data) {
                      if (data.length === 0) {
                        Toast.show("CallerId Does not Match");
                      } else {
                        Toast.show("CallerId Match!");
                        const { token, platform, APN } = data[0]?.data();
                        //initiateCall() is used to send a notification to the receiving user and start the call. 
                        initiateCall({
                          callerInfo: {
                            name: "Person A",
                            ...firebaseUserConfig,
                          },
                          calleeInfo: {
                            token,
                            platform,
                            APN,
                          },
                          videoSDKInfo: {
                            token: videosdkTokenRef.current,
                            meetingId: videosdkMeetingRef.current,
                          },
                        });
                        setisCalling(true);
                      }
                    }
                  } else {
                    Toast.show("Please provide CallerId");
                  }
                }}
                style={{
                  height: 50,
                  backgroundColor: "#5568FE",
                  justifyContent: "center",
                  alignItems: "center",
                  borderRadius: 12,
                  marginTop: 16,
                }}
              >
                <Text
                  style={{
                    fontSize: 16,
                    color: "#FFFFFF",
                  }}
                >
                  Call Now
                </Text>
              </TouchableOpacity>
            </View>
          </>
        </TouchableWithoutFeedback>
      ) : (
        <View style={{ flex: 1, justifyContent: "space-around" }}>
          <View
            style={{
              padding: 35,
              justifyContent: "center",
              alignItems: "center",
              borderRadius: 14,
            }}
          >
            <Text
              style={{
                fontSize: 16,
                color: "#D0D4DD",
              }}
            >
              Calling to...
            </Text>

            <Text
              style={{
                fontSize: 36,
                marginTop: 12,
                color: "#ffff",
                letterSpacing: 8,
              }}
            >
              {number}
            </Text>
          </View>
          <View
            style={{
              justifyContent: "center",
              alignItems: "center",
            }}
          >
            <TouchableOpacity
              onPress={async () => {
                //getCallee is used to get the detials of the user you are trying to intiate a call with
                const data = await getCallee(number);
                if (data) {
                  updateCallStatus({
                    callerInfo: data[0]?.data(),
                    type: "DISCONNECT",
                  });
                  setisCalling(false);
                }
              }}
              style={{
                backgroundColor: "#FF5D5D",
                borderRadius: 30,
                height: 60,
                aspectRatio: 1,
                justifyContent: "center",
                alignItems: "center",
              }}
            >
              <CallEnd width={50} height={12} />
            </TouchableOpacity>
          </View>
        </View>
      )}
    </KeyboardAvoidingView>
  );
}
Не волнуйтесь, если вы увидите всплывающую ошибку, так как мы скоро добавим методы.

В приведенном выше коде вы столкнетесь со следующими методами:

  • getCallee(): getCallee() используется для получения сведений о пользователе, с которым вы пытаетесь инициировать вызов.
  • initiateCall(): InitialCall() используется для отправки уведомления принимающему пользователю и начала вызова.
  • updateCallStatus(): updateCallStatus() используется для обновления статуса входящего вызова, например, принятого, отклоненного и т. д.

Имея пользовательский интерфейс для вызова на месте, давайте начнем с фактической разработки вызова.

Вот как будет выглядеть пользовательский интерфейс:

Обмен сообщениями Firebase для инициирования вызовов

Первым шагом в установлении вызова является идентификация каждого пользователя и получение токена обмена сообщениями для пользователя, который позволит нам отправлять им уведомления.

Итак, на домашней странице нашего приложения мы получим токен обмена сообщениями Firebase. Используя эти токены, мы запросим базу данных firestore, присутствует ли пользователь в базе данных или нет. Если пользователь присутствует, мы обновим состояние firebaseUserConfig в приложении, в противном случае мы зарегистрируем пользователя в базе данных и обновим затем состояние.

  useEffect(() => {
    async function getFCMtoken() {
      const authStatus = await messaging().requestPermission();
      const enabled =
        authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
        authStatus === messaging.AuthorizationStatus.PROVISIONAL;

      if (enabled) {
        const token = await messaging().getToken();
        const querySnapshot = await firestore()
          .collection("users")
          .where("token", "==", token)
          .get();

        const uids = querySnapshot.docs.map((doc) => {
          if (doc && doc?.data()?.callerId) {
            const { token, platform, callerId } = doc?.data();
            setfirebaseUserConfig({
              callerId,
              token,
              platform,
            });
          }
          return doc;
        });

        if (uids && uids.length == 0) {
          addUser({ token });
        } else {
          console.log("Token Found");
        }
      }
    }

    getFCMtoken();
  }, []);

const addUser = ({ token }) => {
    const platform = Platform.OS === "android" ? "ANDROID" : "iOS";
    const obj = {
      callerId: Math.floor(10000000 + Math.random() * 90000000).toString(),
      token,
      platform,
    };
    firestore()
      .collection("users")
      .add(obj)
      .then(() => {
        setfirebaseUserConfig(obj);
        console.log("User added!");
      });
  };

Мы настроим токен Video SDK и идентификатор встречи при загрузке главного экрана, чтобы он был готов, когда пользователь захочет начать вызов.


  const [videosdkToken, setVideosdkToken] = useState(null);
  const [videosdkMeeting, setVideosdkMeeting] = useState(null);

  const videosdkTokenRef = useRef();
  const videosdkMeetingRef = useRef();
  videosdkTokenRef.current = videosdkToken;
  videosdkMeetingRef.current = videosdkMeeting;

  useEffect(() => {
    async function getTokenAndMeetingId() {
      const videoSDKtoken = getToken();
      const videoSDKMeetingId = await createMeeting({ 
        token: videoSDKtoken
      });
      setVideosdkToken(videoSDKtoken);
      setVideosdkMeeting(videoSDKMeetingId);
    }
    getTokenAndMeetingId();
  }, []);

Мы должны создать getToken() и createMeeting(), использованные на предыдущем шаге, в файле src/api/api.js.

const API_BASE_URL = "https://api.videosdk.live/v2";
const VIDEOSDK_TOKEN = "UPDATE YOUR VIDEOSDK TOKEN HERE WHICH YOU GENERATED FROM DASHBOARD ";

export const getToken = () => {
  return VIDEOSDK_TOKEN;
};

export const createMeeting = async ({ token }) => {
  const url = `${API_BASE_URL}/rooms`;
  const options = {
    method: "POST",
    headers: { Authorization: token, "Content-Type": "application/json" },
  };

  const { roomId } = await fetch(url, options)
    .then((response) => response.json())
    .catch((error) => console.error("error", error));

  return roomId;
};

Следующий шаг - инициировать вызов. И для достижения этого нам придется создать два API в качестве функций firebase, которые будут запускать уведомления на другом устройстве и обновлять статус вызова независимо от того, был ли он отклонен или принят.

Создайте эти API в файле functions/index.js.

const functions = require("firebase-functions");
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
var fcm = require("fcm-notification");
var FCM = new fcm("./serviceAccountKey.json");
const app = express();
const { v4: uuidv4 } = require("uuid");

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(morgan("dev"));

//

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.post("/initiate-call", (req, res) => {
  const { calleeInfo, callerInfo, videoSDKInfo } = req.body;

  if (calleeInfo.platform === "ANDROID") {
    var FCMtoken = calleeInfo.token;
    const info = JSON.stringify({
      callerInfo,
      videoSDKInfo,
      type: "CALL_INITIATED",
    });
    var message = {
      data: {
        info,
      },
      android: {
        priority: "high",
      },
      token: FCMtoken,
    };
    FCM.send(message, function (err, response) {
      if (err) {
        res.status(200).send(response);
      } else {
        res.status(400).send(response);
      }
    });
  } else {
    res.status(400).send("Not supported platform");
  }
});

app.post("/update-call", (req, res) => {
  const { callerInfo, type } = req.body;
  const info = JSON.stringify({
    callerInfo,
    type,
  });

  var message = {
    data: {
      info,
    },
    apns: {
      headers: {
        "apns-priority": "10",
      },
      payload: {
        aps: {
          badge: 1,
        },
      },
    },
    token: callerInfo.token,
  };

  FCM.send(message, function (err, response) {
    if (err) {
      res.status(200).send(response);
    } else {
      res.status(400).send(response);
    }
  });
});

app.listen(9000, () => {
  console.log(`API server listening at http://localhost:9000`);
});

exports.app = functions.https.onRequest(app);
  • initiate-call: Initial-call используется для отправки уведомления принимающему пользователю и начала вызова путем отправки сведений, таких как информация о вызывающем абоненте и сведения о комнате VideoSDK.
  • update-call: update-call используется для обновления статуса входящего вызова, например, принятого, отклоненного и т. д., и отправки уведомления вызывающему абоненту.

Теперь, когда API созданы, мы будем запускать их из приложения. Обновите src/api/api.js, указав следующие вызовы API.

Здесь FCM_SERVER_URL необходимо обновить URL-адресом ваших функций firebase.

Вы получите их при развертывании функций или при запуске функций в локальной среде с помощью npm run serve

const FCM_SERVER_URL = "YOUR_FCM_URL";

export const initiateCall = async ({
  callerInfo,
  calleeInfo,
  videoSDKInfo,
}) => {
  await fetch(`${FCM_SERVER_URL}/initiate-call`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      callerInfo,
      calleeInfo,
      videoSDKInfo,
    }),
  })
    .then((response) => {
      console.log(" RESP", response);
    })
    .catch((error) => console.error("error", error));
};

export const updateCallStatus = async ({ callerInfo, type }) => {
  await fetch(`${FCM_SERVER_URL}/update-call`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      callerInfo,
      type,
    }),
  })
    .then((response) => {
      console.log("##RESP", response);
    })
    .catch((error) => console.error("error", error));
};

С помощью этих уведомлений настраивается отправка уведомлений. Теперь нам нужно будет вызвать вызов, когда вы получите уведомление, в этом случае на сцену выходит React-Native Call Keep.

Интеграция с поддержкой звонков

Прежде чем инициировать вызов, нам нужно будет запросить несколько разрешений, а также настроить React-Native Call Keep. Для этого обновите App.js следующим кодом.

  useEffect(() => {
    const options = {
      ios: {
        appName: "VideoSDK",
      },
      android: {
        alertTitle: "Permissions required",
        alertDescription:
          "This application needs to access your phone accounts",
        cancelButton: "Cancel",
        okButton: "ok",
        imageName: "phone_account_icon",
      },
    };
    RNCallKeep.setup(options);
    RNCallKeep.setAvailable(true);

    if (Platform.OS === "android") {
      OverlayPermissionModule.requestOverlayPermission();
    }
  }, []);

Они запросят разрешения Overlay для устройств Android, а также настроят библиотеку CallKeep. Вот ссылка на то, как предоставить эти разрешения.

Возможно, вы помните, что мы настроили приложение для отправки уведомлений о сообщениях, но не добавили никакого прослушивателя для этих уведомлений. Итак, давайте добавим этих слушателей и покажем пользовательский интерфейс вызова при получении уведомления.

Обновите utils/incoming-video-call.js, который будет обрабатывать все функции, связанные с входящим вызовом.

import { Platform } from "react-native";
import RNCallKeep from "react-native-callkeep";
import uuid from "react-native-uuid";

class IncomingCall {
  constructor() {
    this.currentCallId = null;
  }

  configure = (incomingcallAnswer, endIncomingCall) => {
    try {
      this.setupCallKeep();
      Platform.OS === "android" && RNCallKeep.setAvailable(true);
      RNCallKeep.addEventListener("answerCall", incomingcallAnswer);
      RNCallKeep.addEventListener("endCall", endIncomingCall);
    } catch (error) {
      console.error("initializeCallKeep error:", error?.message);
    }
  };

  //These emthod will setup the call keep.
  setupCallKeep = () => {
    try {
      RNCallKeep.setup({
        ios: {
          appName: "VideoSDK",
          supportsVideo: false,
          maximumCallGroups: "1",
          maximumCallsPerCallGroup: "1",
        },
        android: {
          alertTitle: "Permissions required",
          alertDescription:
            "This application needs to access your phone accounts",
          cancelButton: "Cancel",
          okButton: "Ok",
        },
      });
    } catch (error) {
      console.error("initializeCallKeep error:", error?.message);
    }
  };

  // Use startCall to ask the system to start a call - Initiate an outgoing call from this point
  startCall = ({ handle, localizedCallerName }) => {
    // Your normal start call action
    RNCallKeep.startCall(this.getCurrentCallId(), handle, localizedCallerName);
  };

  reportEndCallWithUUID = (callUUID, reason) => {
    RNCallKeep.reportEndCallWithUUID(callUUID, reason);
  };

  //These method will end the incoming call
  endIncomingcallAnswer = () => {
    RNCallKeep.endCall(this.currentCallId);
    this.currentCallId = null;
    this.removeEvents();
  };

  //These method will remove all the event listeners
  removeEvents = () => {
    RNCallKeep.removeEventListener("answerCall");
    RNCallKeep.removeEventListener("endCall");
  };

  //These method will display the incoming call
  displayIncomingCall = (callerName) => {
    Platform.OS === "android" && RNCallKeep.setAvailable(false);
    RNCallKeep.displayIncomingCall(
      this.getCurrentCallId(),
      callerName,
      callerName,
      "number",
      true,
      null
    );
  };

  //Bring the app to foreground
  backToForeground = () => {
    RNCallKeep.backToForeground();
  };

  //Return the ID of current Call
  getCurrentCallId = () => {
    if (!this.currentCallId) {
      this.currentCallId = uuid.v4();
    }
    return this.currentCallId;
  };

  //These Method will end the call
  endAllCall = () => {
    RNCallKeep.endAllCalls();
    this.currentCallId = null;
    this.removeEvents();
  };

}

export default Incomingvideocall = new IncomingCall();
Примечание: Проверьте комментарии к коду, чтобы узнать о функции каждого метода

Мы должны добавить прослушиватель уведомлений в firebase, с помощью которого мы будем вызывать CallKeep для обработки пользовательского интерфейса вызова, что мы можем сделать, добавив следующий код в src/home/index.js.


  useEffect(() => {
    const unsubscribe = messaging().onMessage((remoteMessage) => {
      const { callerInfo, videoSDKInfo, type } = JSON.parse(
        remoteMessage.data.info
      );
      switch (type) {
        case "CALL_INITIATED":
          const incomingCallAnswer = ({ callUUID }) => {
            updateCallStatus({
              callerInfo,
              type: "ACCEPTED",
            });
            Incomingvideocall.endIncomingcallAnswer(callUUID);
            setisCalling(false);
            Linking.openURL(
              `videocalling://meetingscreen/${videoSDKInfo.token}/${videoSDKInfo.meetingId}`
            ).catch((err) => {
              Toast.show(`Error`, err);
            });
          };

          const endIncomingCall = () => {
            Incomingvideocall.endIncomingcallAnswer();
            updateCallStatus({ callerInfo, type: "REJECTED" });
          };

          Incomingvideocall.configure(incomingCallAnswer, endIncomingCall);
          Incomingvideocall.displayIncomingCall(callerInfo.name);

          break;
        case "ACCEPTED":
          setisCalling(false);
          navigation.navigate(SCREEN_NAMES.Meeting, {
            name: "Person B",
            token: videosdkTokenRef.current,
            meetingId: videosdkMeetingRef.current,
          });
          break;
        case "REJECTED":
          Toast.show("Call Rejected");
          setisCalling(false);
          break;
        case "DISCONNECT":
          Platform.OS === "ios"
            ? Incomingvideocall.endAllCall()
            : Incomingvideocall.endIncomingcallAnswer();
          break;
        default:
          Toast.show("Call Could not placed");
      }
    });

    return () => {
      unsubscribe();
    };
  }, []);

 //Used to get the detials of the user you are trying to intiate a call with.
  const getCallee = async (num) => {
    const querySnapshot = await firestore()
      .collection("users")
      .where("callerId", "==", num.toString())
      .get();
    return querySnapshot.docs.map((doc) => {
      return doc;
    });
  };

После добавления приведенного выше кода вы можете заметить, что когда приложение находится на переднем плане, UI вызова работает должным образом, но не когда приложение находится в фоновом режиме. Таким образом, чтобы обработать случай в фоновом режиме, нам нужно будет добавить фоновый прослушиватель для уведомлений. Чтобы добавить слушателя, добавьте указанный ниже код в файл index.js вашего проекта.


const firebaseListener = async (remoteMessage) => {
  const { callerInfo, videoSDKInfo, type } = JSON.parse(
    remoteMessage.data.info
  );

  if (type === "CALL_INITIATED") {
    const incomingCallAnswer = ({ callUUID }) => {
      Incomingvideocall.backToForeground();
      updateCallStatus({
        callerInfo,
        type: "ACCEPTED",
      });
      Incomingvideocall.endIncomingcallAnswer(callUUID);
      Linking.openURL(
        `videocalling://meetingscreen/${videoSDKInfo.token}/${videoSDKInfo.meetingId}`
      ).catch((err) => {
        Toast.show(`Error`, err);
      });
    };

    const endIncomingCall = () => {
      Incomingvideocall.endIncomingcallAnswer();
      updateCallStatus({ callerInfo, type: "REJECTED" });
    };

    Incomingvideocall.configure(incomingCallAnswer, endIncomingCall);
    Incomingvideocall.displayIncomingCall(callerInfo.name);
    Incomingvideocall.backToForeground();
  }
};

// Register background handler
messaging().setBackgroundMessageHandler(firebaseListener);

Вот как будет выглядеть входящий и исходящий вызов:

Вы только что реализовали функцию вызова, которая работает как шарм.

Но без видеозвонка он по-прежнему кажется неполным. Что ж, для этого у нас есть VideoSDK, который мы реализуем на следующих этапах.

Интеграция VideoSDK

Мы будем показывать видеовызов на экране собрания, который мы создали ранее. На этом экране будет создан раздел комнаты до присоединения к собранию, а после этого удаленный участник будет отображаться в большом виде, а локальный участник — в мини-виде. У нас будет три кнопки для переключения микрофона, веб-камеры и выхода из звонка.

.
└── scenes/
    ├── home/
    └── meeting/
        ├── OneToOne/
        │ ├── LargeView/
        │ │ └── index.js
        │ ├── MiniView/
        │ │ └── index.js
        │ └── index.js
        ├── index.js
        └── MeetingContainer.js

Первым шагом в интеграции VideoSDK является добавление MeetingProvider в src/scene/meeting/index.js, который инициирует собрание и присоединится к нему.

import React from "react";
import { Platform, SafeAreaView } from "react-native";
import colors from "../../styles/colors";
import {
  MeetingConsumer,
  MeetingProvider,
} from "@videosdk.live/react-native-sdk";
import MeetingContainer from "./MeetingContainer";
import { SCREEN_NAMES } from "../../navigators/screenNames";
import IncomingVideoCall from "../../utils/incoming-video-call";

export default function ({ navigation, route }) {
  const token = route.params.token;
  const meetingId = route.params.meetingId;
  const micEnabled = route.params.micEnabled ? route.params.micEnabled : true;
  const webcamEnabled = route.params.webcamEnabled
    ? route.params.webcamEnabled
    : true;
  const name = route.params.name;

  return (
    <SafeAreaView
      style={{ flex: 1, backgroundColor: colors.primary[900], padding: 12 }}
    >
      <MeetingProvider
        config={{
          meetingId: meetingId,
          micEnabled: micEnabled,
          webcamEnabled: webcamEnabled,
          name: name,
          notification: {
            title: "Video SDK Meeting",
            message: "Meeting is running.",
          },
        }}
        token={token}
      >
        <MeetingConsumer
          {...{
            onMeetingLeft: () => {
              Platform.OS == "ios" && IncomingVideoCall.endAllCall();
              navigation.navigate(SCREEN_NAMES.Home);
            },
          }}
        >
          {() => {
            return <MeetingContainer webcamEnabled={webcamEnabled} />;
          }}
        </MeetingConsumer>
      </MeetingProvider>
    </SafeAreaView>
  );
}

Мы использовали компонент MeetingContainer, который будет содержать различные макеты для нашей встречи, такие как отображение «Ожидание присоединения» до присоединения к собранию, а также полное представление собрания после присоединения к собранию.

import {
  useMeeting,
  ReactNativeForegroundService,
} from "@videosdk.live/react-native-sdk";
import { useEffect, useState } from "react";
import OneToOneMeetingViewer from "./OneToOne";
import WaitingToJoinView from "./Components/WaitingToJoinView";
import React from "react";
import { convertRFValue } from "../../../styles/spacing";
import { Text, View } from "react-native";
import colors from "../../../styles/colors";

export default function MeetingContainer({ webcamEnabled }) {
  const [isJoined, setJoined] = useState(false);

  const { join, changeWebcam, participants, leave } = useMeeting({
    onMeetingJoined: () => {
      setTimeout(() => {
        setJoined(true);
      }, 500);
    },
  });

  useEffect(() => {
    setTimeout(() => {
      if (!isJoined) {
        join();
        if (webcamEnabled) changeWebcam();
      }
    }, 1000);

    return () => {
      leave();
      ReactNativeForegroundService.stopAll();
    };
  }, []);

  return isJoined ? (
    <OneToOneMeetingViewer />
  ) : (
    <View
      style={{
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        height: "100%",
        width: "100%",
      }}
    >
      <Text
        style={{
          fontSize: convertRFValue(18),
          color: colors.primary[100],
          marginTop: 28,
        }}
      >
        Creating a room
      </Text>
    </View>
  );
}

Затем мы добавим наш MeetingView, который будет отображать кнопки и представление участников в файле src/scenes/meeting/OneToOne/index.js.

import React from "react";
import {
  View, Text,Clipboard, TouchableOpacity, ActivityIndicator,
} from "react-native";
import { useMeeting } from "@videosdk.live/react-native-sdk";
import { 
    CallEnd, CameraSwitch, Copy, MicOff, MicOn, VideoOff, VideoOn,
} from "../../../assets/icons";
import colors from "../../../styles/colors";
import IconContainer from "../../../components/IconContainer";
import LocalViewContainer from "./LocalViewContainer";
import LargeView from "./LargeView";
import MiniView from "./MiniView";
import Toast from "react-native-simple-toast";

export default function OneToOneMeetingViewer() {
  const {
    participants,
    localWebcamOn,
    localMicOn,
    leave,
    changeWebcam,
    toggleWebcam,
    toggleMic,
    meetingId,
  } = useMeeting({
    onError: (data) => {
      const { code, message } = data;
      Toast.show(`Error: ${code}: ${message}`);
    },
  });

  const participantIds = [...participants.keys()];

  const participantCount = participantIds ? participantIds.length : null;

  return (
    <>
      <View
        style={{
          flexDirection: "row",
          alignItems: "center",
          width: "100%",
        }}
      >
        <View
          style={{
            flex: 1,
            justifyContent: "space-between",
          }}
        >
          <View style={{ flexDirection: "row" }}>
            <Text
              style={{
                fontSize: 16,
                color: colors.primary[100],
              }}
            >
              {meetingId ? meetingId : "xxx - xxx - xxx"}
            </Text>

            <TouchableOpacity
              style={{
                justifyContent: "center",
                marginLeft: 10,
              }}
              onPress={() => {
                Clipboard.setString(meetingId);
                Toast.show("Meeting Id copied Successfully");
              }}
            >
              <Copy fill={colors.primary[100]} width={18} height={18} />
            </TouchableOpacity>
          </View>
        </View>
        <View>
          <TouchableOpacity
            onPress={() => {
              changeWebcam();
            }}
          >
            <CameraSwitch height={26} width={26} fill={colors.primary[100]} />
          </TouchableOpacity>
        </View>
      </View>
      {/* Center */}
      <View style={{ flex: 1, marginTop: 8, marginBottom: 12 }}>
        {participantCount > 1 ? (
          <>
            <LargeView participantId={participantIds[1]} />
            <MiniView participantId={participantIds[0]} />
          </>
        ) : participantCount === 1 ? (
          <LargeView participantId={participantIds[0]} />
        ) : (
          <View
            style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
          >
            <ActivityIndicator size={"large"} />
          </View>
        )}
      </View>
      {/* Bottom */}
      <View
        style={{
          flexDirection: "row",
          justifyContent: "space-evenly",
        }}
      >
        <IconContainer
          backgroundColor={"red"}
          onPress={() => {
            leave();
          }}
          Icon={() => {
            return <CallEnd height={26} width={26} fill="#FFF" />;
          }}
        />
        <IconContainer
          style={{
            borderWidth: 1.5,
            borderColor: "#2B3034",
          }}
          backgroundColor={!localMicOn ? colors.primary[100] : "transparent"}
          onPress={() => {
            toggleMic();
          }}
          Icon={() => {
            return localMicOn ? (
              <MicOn height={24} width={24} fill="#FFF" />
            ) : (
              <MicOff height={28} width={28} fill="#1D2939" />
            );
          }}
        />
        <IconContainer
          style={{
            borderWidth: 1.5,
            borderColor: "#2B3034",
          }}
          backgroundColor={!localWebcamOn ? colors.primary[100] : "transparent"}
          onPress={() => {
            toggleWebcam();
          }}
          Icon={() => {
            return localWebcamOn ? (
              <VideoOn height={24} width={24} fill="#FFF" />
            ) : (
              <VideoOff height={36} width={36} fill="#1D2939" />
            );
          }}
        />
      </View>
    </>
  );
}

Здесь мы показываем участников в двух разных представлениях: во-первых, если есть один участник, мы показываем локального участника в полноэкранном режиме, а во-вторых, когда участников двое, мы показываем локального участника в MiniView.

Чтобы достичь этого, вам необходимо выполнить следующие два компонента:

  • src/scenes/meeting/OneToOne/LargeView/index.js
import { useParticipant, RTCView, MediaStream } from "@videosdk.live/react-native-sdk";
import React, { useEffect } from "react";
import { View } from "react-native";
import colors from "../../../../styles/colors";
import Avatar from "../../../../components/Avatar";

export default LargeViewContainer = ({ participantId }) => {
  const { webcamOn, webcamStream, displayName, setQuality, isLocal } =
    useParticipant(participantId, {});

  useEffect(() => {
    setQuality("high");
  }, []);

  return (
    <View
      style={{
        flex: 1,
        backgroundColor: colors.primary[800],
        borderRadius: 12,
        overflow: "hidden",
      }}
    >
      {webcamOn && webcamStream ? (
        <RTCView
          objectFit={'cover'}
          mirror={isLocal ? true : false}
          style={{ flex: 1, backgroundColor: "#424242" }}
          streamURL={new MediaStream([webcamStream.track]).toURL()}
        />
      ) : (
        <Avatar
          containerBackgroundColor={colors.primary[800]}
          fullName={displayName}
          fontSize={26}
          style={{
            backgroundColor: colors.primary[700],
            height: 70,
            aspectRatio: 1,
            borderRadius: 40,
          }}
        />
      )}
    </View>
  );
};
  • src/scenes/meeting/OneToOne/MiniView/index.js
import { useParticipant, RTCView, MediaStream } from "@videosdk.live/react-native-sdk";
import React, { useEffect } from "react";
import { View } from "react-native";
import Avatar from "../../../../components/Avatar";
import colors from "../../../../styles/colors";

export default MiniViewContainer = ({ participantId }) => {
  const { webcamOn, webcamStream, displayName, setQuality, isLocal } =
    useParticipant(participantId, {});

  useEffect(() => {
    setQuality("high");
  }, []);

  return (
    <View
      style={{
        position: "absolute",
        bottom: 10,
        right: 10,
        height: 160,
        aspectRatio: 0.7,
        borderRadius: 8,
        borderColor: "#ff0000",
        overflow: "hidden",
      }}
    >
      {webcamOn && webcamStream ? (
        <RTCView
          objectFit="cover"
          zOrder={1}
          mirror={isLocal ? true : false}
          style={{ flex: 1, backgroundColor: "#424242" }}
          streamURL={new MediaStream([webcamStream.track]).toURL()}
        />
      ) : (
        <Avatar
          fullName={displayName}
          containerBackgroundColor={colors.primary[600]}
          fontSize={24}
          style={{
            backgroundColor: colors.primary[500],
            height: 60,
            aspectRatio: 1,
            borderRadius: 40,
          }}
        />
      )}
    </View>
  );
};

Вот как будет выглядеть видеозвонок с двумя участниками:

Заключение

Благодаря этому мы успешно создали приложение для видеозвонков React Native с Callkeep, используя видео SDK и Firebase. Вы всегда можете обратиться к нашей документации, если хотите добавить такие функции, как обмен сообщениями в чате и совместное использование экрана.

#JavaScript #React #ReactNative
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу