在一个我们都通过音频和视频通话连接的世界中,如果您打算制作一个这样的应用程序,那么您已经降落在正确的位置。
我们将在React Native中构建一个完整的视频通话应用程序,这将使您可以无缝进行视频通话。我们将使用VideoSDK进行视频会议,并进行反应本机呼叫kepect来管理呼叫UI。这是一个由两部分组成的系列,我们将首先在Android中实现呼叫kepke,然后对iOS进行配置。
现在所有的要求都得到了很好的解释,让我们直接潜入有趣的部分,但是如果您太渴望看到结果,这是link to test the app和complete code for the app。
什么是通话?
呼叫keeck是一个反应本机库,允许您在应用程序的任何给定状态,即前景(运行),背景,戒烟,锁定设备等处处理Android和iOS设备上的传入call ui。 p>
在构建应用程序之前,您应该知道该应用程序在内部的运行方式,这反过来将有助于轻松开发过程。
该应用程序将如何功能?
要更好地了解该应用程序的功能,让我们以约翰想打电话给他的朋友麦克斯的情况。约翰将首先打开我们的应用程序,他将在那里输入Max的呼叫者ID并命中呼叫。麦克斯将在他的手机上看到一个来电UI,他可以接受或拒绝电话。一旦他接受了电话,我们将使用Videosdk之间设置它们之间的React Native video call。
您可能会认为这些非常简单。好吧,让我们更多地详细介绍实施的细微差别。
- 当约翰输入Max的呼叫者ID并键入呼叫按钮时,我们要做的第一件事是将其映射到我们的Firebase数据库并在其设备上发送通知。
- 当Max的设备收到这些通知时,我们的应用程序的逻辑将使用React Native Callkeame库向他展示传入的ui。
- 当Max接受或拒绝传入电话时,我们将使用通知将状态发送给John,并最终启动它们之间的视频通话。
- 这是流动的图形表示,以更好地理解。
现在,我们已经建立了应用程序的流程及其功能的运行方式,让我们开始开发而无需更多的聊天。
应用程序的要求和库
首先,让我们看一下我们将使用的一组库来建立应用程序的功能。
- React Native CallKeep:这些库将有助于在设备上调用来电。
- React Native VoIP Push Notification:这些库用于在iOS设备上发送推送通知,因为该应用程序处于遇到状态时,燃料底通知在iOS设备上无法正常运行。
- VideoSDK RN Android Overlay Permission:这些库将处理较新的Android版本的覆盖权,确保始终可见来电。
- React Native Firebase Messaging:这些库用于发送和接收Firebase通知,该通知将调用我们的来电UI。
- 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在呈现的字段中,然后单击注册应用程序。
4.下载google-services.json
文件并将其移至android/app
5.按照显示的步骤将firebase SDK添加到Android应用中。
6.在您的Firebase项目中创建一个新的Web应用程序,该应用将用于访问Firestore数据库。
7.在项目中的database/firebaseDb.js
文件中显示的配置文件。
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
。
使用这些,我们已经完成了运行应用程序所需的设置。
应用程序侧代码
让我们继续在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中获得所有访问所有icons和components。
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()用于更新传入呼叫的状态,例如接受,被拒绝等。
- 使用UI进行呼叫,让我们从实际的通话开发开始。
这些是UI的外观:
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
:更新通话用于更新传入呼叫的状态,例如接受,拒绝等,并将通知发送给呼叫者。
- 现在创建了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设备的覆盖权限,并设置Callkeep库。这是对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);
在这里,传入和传出电话的样子是:
哇!您刚刚实现了像魅力一样工作的调用功能。
但是没有视频呼叫仍然感觉不完整。好吧,为此,我们拥有将在即将到来的步骤中实现的视频。
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>
);
};
这些视频呼叫与两个参与者的样子:
欢呼!!!有了这些,我们的视频通话功能已完成。这是视频的外观。
前往系列的第二部分,了解如何配置iOS接收呼叫并启动视频呼叫。
结论
这样,我们使用视频SDK和Firebase成功地使用CALLEKEK构建了React Native Video Calleph应用程序。如果您想添加聊天消息和屏幕共享等功能,请随时参考我们的documentation。如果您在实施方面有任何问题,请通过我们的Discord community与我们联系。