如何使用视频SDK构建用呼叫kephek构建反应本机视频通话应用
#javascript #react #reactnative #webrtc

在一个我们都通过音频和视频通话连接的世界中,如果您打算制作一个这样的应用程序,那么您已经降落在正确的位置。

我们将在React Native中构建一个完整的视频通话应用程序,这将使您可以无缝进行视频通话。我们将使用VideoSDK进行视频会议,并进行反应本机呼叫kepect来管理呼叫UI。这是一个由两部分组成的系列,我们将首先在Android中实现呼叫kepke,然后对iOS进行配置。

现在所有的要求都得到了很好的解释,让我们直接潜入有趣的部分,但是如果您太渴望看到结果,这是link to test the appcomplete code for the app

什么是通话?

呼叫keeck是一个反应本机库,允许您在应用程序的任何给定状态,即前景(运行),背景,戒烟,锁定设备等处处理Android和iOS设备上的传入call ui。 p>

在构建应用程序之前,您应该知道该应用程序在内部的运行方式,这反过来将有助于轻松开发过程。

该应用程序将如何功能?

要更好地了解该应用程序的功能,让我们以约翰想打电话给他的朋友麦克斯的情况。约翰将首先打开我们的应用程序,他将在那里输入Max的呼叫者ID并命中呼叫。麦克斯将在他的手机上看到一个来电UI,他可以接受或拒绝电话。一旦他接受了电话,我们将使用Videosdk之间设置它们之间的React Native video call

您可能会认为这些非常简单。好吧,让我们更多地详细介绍实施的细微差别。

  1. 当约翰输入Max的呼叫者ID并键入呼叫按钮时,我们要做的第一件事是将其映射到我们的Firebase数据库并在其设备上发送通知。
  2. 当Max的设备收到这些通知时,我们的应用程序的逻辑将使用React Native Callkeame库向他展示传入的ui。
  3. 当Max接受或拒绝传入电话时,我们将使用通知将状态发送给John,并最终启动它们之间的视频通话。
  4. 这是流动的图形表示,以更好地理解。

这是流动的图形表示,以更好地理解。
Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK

现在,我们已经建立了应用程序的流程及其功能的运行方式,让我们开始开发而无需更多的聊天。

应用程序的要求和库
首先,让我们看一下我们将使用的一组库来建立应用程序的功能。

  1. React Native CallKeep:这些库将有助于在设备上调用来电。
  2. React Native VoIP Push Notification:这些库用于在iOS设备上发送推送通知,因为该应用程序处于遇到状态时,燃料底通知在iOS设备上无法正常运行。
  3. VideoSDK RN Android Overlay Permission:这些库将处理较新的Android版本的覆盖权,确保始终可见来电。
  4. React Native Firebase Messaging:这些库用于发送和接收Firebase通知,该通知将调用我们的来电UI。
  5. React Native Firebase Firestore:这些库用于存储呼叫者ID和设备令牌,将用于建立视频呼叫。

如果我们查看开发要求,这是您需要的:

  • node.js v12+
  • NPM V6+(随附新节点版本)
  • 安装了Android Studio和Xcode。
  • A Video SDK Token (Dashboard > Api-Key) (Video Tutorial)
  • 测试调用功能至少需要两个物理设备。

客户端设置用于本机Android应用程序

让我们首先使用命令创建一个新的React Native应用程序:

npx react-native init VideoSdkCallKeepExample

现在创建了我们的基本应用程序,让我们从安装所有依赖项开始。

1.首先,我们将安装@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

2.我们依赖列表中的第二个是视频库,它将为应用程序提供视频会议。

npm install "@videosdk.live/react-native-sdk"
npm install "@videosdk.live/react-native-incallmanager"

3.next将安装与Firebase相关的依赖项。

npm install @react-native-firebase/app
npm install @react-native-firebase/messaging
npm install @react-native-firebase/firestore
npm install firebase

4.最后,React本机呼叫库和推送通知和权限所需的其他库。

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

注意:我们已经使用github存储库链接将REECT本机呼叫库的引用放置,因为NPM版本已与Android构建问题。

我们都与我们的依赖关系设置。现在让我们从我们安装的所有库的Android设置开始。

反应本机Android设置

VideosDK设置

1.LETS首先在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>

2.在app级构建中的以下行。

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

3.添加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')

4.用以下软件包对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;
  }
}

5.lastly在index.js文件中的应用程序中注册VideoSDK服务。

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

// Register the VideoSDK service
register();

呼叫对本机Android应用程序的呼叫设置

1.LETS首先在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>

React Antial Android应用程序的Firebase设置

1.开始,从这里开始创建一个新的firebase项目。
2.创建项目后,通过单击Android图标,将您的React Antive Android应用程序添加到Firebase项目中。
3.添加您的应用程序applicationID在呈现的字段中,然后单击注册应用程序。

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK

4.下载google-services.json文件并将其移至android/app

5.按照显示的步骤将firebase SDK添加到Android应用中。

6.在您的Firebase项目中创建一个新的Web应用程序,该应用将用于访问Firestore数据库。

7.在项目中的database/firebaseDb.js文件中显示的配置文件。

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK

8.到达左图中的firebase firestore并创建一个数据库,我们将使用该数据库存储呼叫者ID。

9.这些都将我们都设置在Android上的firebase。

服务器端设置

现在我们已经完成了应用程序的设置。让我们还设置服务器端API。为了创建这些API,我们将用户使用Firebase功能。因此,让我们直接进入它。

1.到达左图中的firebase功能。要使用Firebase功能,您需要在计划时升级到薪水。尽管您不必担心charges如果您只是作为一个爱好项目建立,因为有慷慨的免费配额。
2.通过使用以下命令安装Firebase CLI,通过Firebase功能开始。

npm install -g firebase-tools

3. run firebase login通过浏览器登录并验证firebase cli。

4.进入您的firebase项目目录。

5. run firebase init functions初始化firebase函数项目,我们将在其中编写API。请按照CLI中显示的设置说明进行操作,并且一旦过程完成后,您应该在目录中看到functions文件夹。

6.从项目设置下载服务帐户密钥,然后将其放入functions/serviceAccountKey.json

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK

使用这些,我们已经完成了运行应用程序所需的设置。

应用程序侧代码

让我们继续在React Native端启动代码。我们将创建两个屏幕,首先是,用户可以在其中看到他的呼叫者ID并输入其他人员呼叫者ID以启动新调用。

我们将遵循以下显示的文件夹结构:

.
└── 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开始。

1.为了让您开头,我们已经创建了我们需要的基本组件,例如按钮,文本场,化身和图标。您可以从我们的github repository中获得所有访问所有iconscomponents
2.使用我们的基本组件设置,让我们将导航屏幕添加到应用程序中。我们将拥有一个主屏幕,该屏幕将具有呼叫者ID输入和一个呼叫按钮和会议屏幕,该屏幕将带有视频通话。


因此,以以下屏幕名称更新src/navigators/screenNames.js

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

3.用导航堆栈填写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>
  );
}

4.准备好我们的导航堆栈,让我们设置主屏幕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():initiateCall()用于向接收用户发送通知并启动呼叫。
  • updateCallStatus():UpdateCallStatus()用于更新传入呼叫的​​状态,例如接受,被拒绝等。
  1. 使用UI进行呼叫,让我们从实际的通话开发开始。

这些是UI的外观:

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK


firebase消息传递以发起呼叫

建立呼叫的第一步是识别每个用户并为用户获取消息传递令牌,这将使我们能够发送通知。

1.在我们应用程序的主页中,我们将获得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!");
      });
  };

2.我们将在主屏幕加载时设置VideoSDK令牌并满足ID


  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();
  }, []);

3.我们必须在src/api/api.js文件中使用上述步骤中使用的getToken()createMeeting()

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;
};

4.接头步骤是启动呼叫。为此,我们将必须创建两个API作为Firebase函数,这些功能将触发另一个设备上的通知并更新呼叫状态,无论是拒绝还是ACCPET。

functions/index.js中创建这些API

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:启动通话用于向接收用户发送通知,并通过发送诸如呼叫者信息和Videosdk Room Detials之类的详细信息来启动呼叫。
  • update-call:更新通话用于更新传入呼叫的​​状态,例如接受,拒绝等,并将通知发送给呼叫者。
  1. 现在创建了API,我们将从应用程序触发它们。使用以下API调用更新src/api/api.js

这里需要使用firebase功能的URL更新FCM_SERVER_URL

当您部署功能或使用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));
};

6.这些通知已设置。现在,当您收到通知时,我们将不得不调用呼叫,这些反应呼叫会持续到图片中。


呼叫保持集成

1.在启动呼叫之前,我们将不得不索取一些权限,还必须设置反应呼叫保留。为此,请使用以下代码更新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();
    }
  }, []);

这些将要求使用Android设备的覆盖权限,并设置Call​​keep库。这是对how to grant these permissions.

的引用

2.您可能还记得我们已经设置了该应用程序来发送消息通知,但没有为这些通知添加任何侦听器。因此,让我们添加这些侦听器并在收到通知时显示呼叫UI。

更新utils/incoming-video-call.js,该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();

注意:检查代码注释以了解每种方法的功能

3.我们必须在firebase上添加通知侦听器,我们将调用呼叫kekek以处理呼叫UI,我们可以通过在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;
    });
  };

4.添加上面的代码后,您可能会观察到,当应用程序处于前景时,呼叫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);

在这里,传入和传出电话的样子是:

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK

哇!您刚刚实现了像魅力一样工作的调用功能。

但是没有视频呼叫仍然感觉不完整。好吧,为此,我们拥有将在即将到来的步骤中实现的视频。


VideosDK集成

1.我们将在我们之前创建的会议屏幕中显示视频通话。这些屏幕将在会议加入之前创建房间部分,此后,它将有远程参与者和小型视图的本地参与者进行远程参与者。我们将有三个按钮可以切换麦克风,切换网络摄像头并留下呼叫。

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

2.集成VideoSDK的第一个步骤是在src/scene/meeting/index.js中添加了MeetingProvider,它将启动会议并加入会议。

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>
  );
}

3.我们使用了会议的组件,该组件将为我们的会议提供不同的布局,例如在会议加入之前等待加入,并且一旦会议加入了会议

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>
  );
}

4.Next我们将添加我们的会议视图,该查看浏览将在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>
    </>
  );
}

5.在这里,我们正在向参与者展示两种不同的观点,首先,如果有一个参与者,我们将向全屏幕展示本地参与者,其次,当有两个参与者时,我们将向当地参与者展示小型。

要实现这些目标,您需要遵循两个组件:

a。 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>
  );
};

a。 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>
  );
};

这些视频呼叫与两个参与者的样子:

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK

欢呼!!!有了这些,我们的视频通话功能已完成。这是视频的外观。

前往系列的第二部分,了解如何配置iOS接收呼叫并启动视频呼叫。

结论

这样,我们使用视频SDK和Firebase成功地使用CALLEKEK构建了React Native Video Calleph应用程序。如果您想添加聊天消息和屏幕共享等功能,请随时参考我们的documentation。如果您在实施方面有任何问题,请通过我们的Discord community与我们联系。