欢迎阅读本综合指南,了解如何使用 WebRTC 和 Firebase 在 React Native 上轻松创建视频通话应用程序。在本教程中,我将一步步带您开发自己的实时视频通信平台。
视频通话已成为现代通信不可或缺的一部分,借助 React Native 的跨平台功能和 WebRTC 在实时媒体流方面的强大功能,您可以构建一个功能丰富的应用程序,将来自不同设备的用户无缝连接起来。
我们将利用 Firebase 这个强大的基于云的平台、信号和数据存储,让我们专注于应用程序的核心功能,而不必担心复杂的后台配置。
在本教程中,你将学习如何设置 react native 开发环境、使用 firestore 配置 Firebase 数据库、集成 nativewind css、通过 WebRTC 建立点对点连接、启用实时视频流以及有效管理通话状态。
那么,让我们开始!使用 React Native、WebRTC、Firebase 和 Tailwind 构建自己的视频通话应用程序!
我们要使用什么?
React Native 是由 Facebook 开发的开源移动应用框架。它允许开发人员使用 JavaScript 和 React(一种流行的前端库)构建跨平台移动应用程序。它是我们视频通话应用程序的基础。它使我们能够创建一个可在 iOS 和 Android 设备上运行的单一代码库,使我们无需为每个平台开发单独的应用程序。
WebRTC 是开源 API 和通信协议的集合,可实现 Web 浏览器和移动应用程序之间的实时点对点通信。它提供音频和视频通话以及数据共享功能,无需安装插件或其他软件。
Firebase 作为一个实时数据库和云平台,为处理 WebRTC 中的信令过程提供了理想的基础架构。当用户要与其他用户发起视频通话时,需要发送信令消息以建立连接。Firebase 的实时数据库提供了实时发送和接收这些信令消息的机制。
Tailwind CSS 是我们将用来设计组件而不是样式表的 CSS。它是一个实用程序优先的 CSS 框架,提供了大量预构建的实用程序类,您可以将它们直接应用于 React 原生元素。这种方法促进了 React Native 中组件样式化的有效方式,类似于 Tailwind CSS 为 Web 开发提供的方式。
设置 React Native Expo
在本教程中,我们将使用 React Native Expo 。不过,请注意 WebRTC 模块包含本地代码,这意味着它默认情况下无法在 Expo Go 应用程序上正常运行。为了解决这个问题,我们将使用 EAS build 创建一个开发构建。我将在软件包安装的最后指导您完成整个过程。
设置完 React Native Expo 后,第一步是安装该项目所需的必要软件包,包括 WebRTC、Firebase 和 Tailwind。当我们完成本教程的后续部分时,我将演示如何安装这些包。
在 React Native 上设置 WebRTC
在 React Native 上设置 WebRTC 涉及几个步骤。以下是在 React Native 项目中开始使用 WebRTC 的说明:
1. 首先使用yarn、npm 或 npx expo install 安装软件包。
npx expo install react-native-webrtc @config-plugins/react-native-webrtc
2. 安装此 npm 包后,将配置插件添加到app.json或app.config.js的 plugins 数组中:
{
"expo": {
"plugins": ["@config-plugins/react-native-webrtc"]
}
}
3. 你可以使用此参考作为指南。
在 React Native 上设置 Firebase
接下来,我们将使用 Firebase 作为我们的信令服务器。请按照以下步骤开始:
1. 登录您的 Firebase 帐户并创建一个新项目。在 Firebase 控制台中,初始化 Cloud Firestore 并在测试模式下创建数据库。
2. 完成此步骤后,进入项目设置 > 常规 > 您的应用程序,注册您的应用程序。注册后,您将收到必要的证书。
3. 使用 npm 使用以下命令安装 Firebase:
npm install firebase
4. 创建一个名为firebase.js的新文件,并将您自己的 Firebase SDK 配置粘贴到其中,如下所示:
//firebase.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// import { getAnalytics } from "firebase/analytics";
// import { getFirestore } from "firebase/firestore";
import { initializeFirestore } from "firebase/firestore";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "Insert here your FIREBASE_API_KEY",
authDomain: "Insert here your FIREBASE_AUTH_DOMAIN",
projectId: "Insert here your FIREBASE_PROJECT_ID",
storageBucket: "Insert here your FIREBASE_STORAGE_BUCKET",
messagingSenderId: "Insert here your FIREBASE_MESSAGING_SENDER_ID",
appId: "Insert here your FIREBASE_APP_ID",
measurementId: "Insert here your FIREBASE_MEASUREMENT_ID",
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// const analytics = getAnalytics(app);
export const db = initializeFirestore(app, {
experimentalForceLongPolling: true,
});
在 React Native 上设置 Tailwind/Nativewind
让我们从文档中安装 Nativewind CSS 的依赖项。
1. 需要安装 nativewind 及其对等依赖项 tailwindcss。
npm install nativewind
npm install --dev tailwindcss
2. 运行npx tailwindcss init
创建tailwind.config.js文件。在tailwind.config.js文件中添加所有组件文件的路径。
//tailwind.config.js
module.exports = {
content: ["./App.{js,jsx,ts,tsx}", "./screens/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
3. Modify your babel.config.js
module.exports = {
plugins: ["nativewind/babel"],
};
3. 修改你的babel.config.js
//babel.config.js
module.exports = {
plugins: ["nativewind/babel"],
};
4. 确保包含您要在内容中创建的文件夹,以便您可以在文件中使用 Nativewind。详细文档链接。
设置 EAS 构建
1. 通过运行以下命令将 EAS CLI 安装为全局 npm 依赖项:
npm install -g eas-cli
2. 初始化开发版本,在项目中安装expo-dev-client库:
npx expo install expo-dev-client
3. 使用以下命令登录您的 expo 帐户:
eas login
4. 运行 eas build 命令创建 eas.json,初始化 EAS Build。
//eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
}
}
5. 在 Android 设备上创建并安装开发版本。每个平台都有必须遵循的特定说明:
eas build --profile development --platform android
6. 访问 Expo 网站并登录以查看你的构建。完成后,需要将其安装在 Android 设备上并在终端中运行以下命令:
Expo start --dev-client
7. 用相机扫描二维码,应用程序将使用您刚刚安装的开发版本打开。这些说明可从此链接引用
这就是我在这个项目中使用的所有包依赖项。你可以将其复制到你的依赖项上并运行命令npm install
。
//Package.json
"dependencies": {
"@config-plugins/react-native-webrtc": "^6.0.0",
"@react-navigation/native": "^6.1.7",
"expo": "~48.0.9",
"expo-dev-client": "~2.1.6",
"expo-status-bar": "~1.4.4",
"firebase": "^9.18.0",
"nativewind": "^2.0.11",
"postcss": "^8.4.23",
"react": "18.2.0",
"react-native": "0.71.4",
"react-native-vector-icons": "^10.0.0",
"react-native-webrtc": "^106.0.7",
"tailwindcss": "^3.3.2"
},
APP.js
这是处理屏幕的地方。默认情况下,为用户呈现的第一个屏幕是房间屏幕。
//App.js
import React, { useState } from "react";
import { Text, SafeAreaView } from "react-native";
import RoomScreen from "./screens/RoomScreen";
import CallScreen from "./screens/CallScreen";
import JoinScreen from "./screens/JoinScreen";
// Just to handle navigation
export default function App() {
const screens = {
ROOM: "JOIN_ROOM",
CALL: "CALL",
JOIN: "JOIN",
};
const [screen, setScreen] = useState(screens.ROOM);
const [roomId, setRoomId] = useState("");
let content;
switch (screen) {
case screens.ROOM:
content = (
<RoomScreen
roomId={roomId}
setRoomId={setRoomId}
screens={screens}
setScreen={setScreen}
/>
);
break;
case screens.CALL:
content = (
<CallScreen roomId={roomId} screens={screens} setScreen={setScreen} />
);
break;
case screens.JOIN:
content = (
<JoinScreen roomId={roomId} screens={screens} setScreen={setScreen} />
);
break;
default:
content = <Text>Wrong Screen</Text>;
}
return (
<SafeAreaView className="flex-1 justify-center ">{content}</SafeAreaView>
);
}
RoomScreen.js
在房间屏幕上,我们创建了一个名为 generateRandomId()
的函数,该函数在输入字段中生成随机房间 ID。但是,你仍然可以根据需要对其进行编辑。
//RoomScreen.js
import React, { useEffect, useState } from "react";
import { Text, View, TextInput, TouchableOpacity, Alert } from "react-native";
import { db } from "../firebase";
import {
addDoc,
collection,
doc,
setDoc,
getDoc,
updateDoc,
onSnapshot,
deleteField,
} from "firebase/firestore";
export default function RoomScreen({ setScreen, screens, setRoomId, roomId }) {
const onCallOrJoin = (screen) => {
if (roomId.length > 0) {
setScreen(screen);
}
};
//generate random room id
useEffect(() => {
const generateRandomId = () => {
const characters = "abcdefghijklmnopqrstuvwxyz";
let result = "";
for (let i = 0; i < 7; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters.charAt(randomIndex);
}
return setRoomId(result);
};
generateRandomId();
}, []);
//checks if room is existing
const checkMeeting = async () => {
if (roomId) {
const roomRef = doc(db, "room", roomId);
const roomSnapshot = await getDoc(roomRef);
// console.log(roomSnapshot.data());
if (!roomSnapshot.exists() || roomId === "") {
// console.log(`Room ${roomId} does not exist.`);
Alert.alert("Wait for your instructor to start the meeting.");
return;
} else {
onCallOrJoin(screens.JOIN);
}
} else {
Alert.alert("Provide a valid Room ID.");
}
};
return (
<View>
<Text className="text-2xl font-bold text-center">Enter Room ID:</Text>
<TextInput
className="bg-white border-sky-600 border-2 mx-5 my-3 p-2 rounded-md"
value={roomId}
onChangeText={setRoomId}
/>
<View className="gap-y-3 mx-5 mt-2">
<TouchableOpacity
className="bg-sky-300 p-2 rounded-md"
onPress={() => onCallOrJoin(screens.CALL)}
>
<Text className="color-black text-center text-xl font-bold ">
Start meeting
</Text>
</TouchableOpacity>
<TouchableOpacity
className="bg-sky-300 p-2 rounded-md"
onPress={() => checkMeeting()}
>
<Text className="color-black text-center text-xl font-bold ">
Join meeting
</Text>
</TouchableOpacity>
</View>
</View>
);
}
checkMeeting 函数用于验证 Firestore 数据库中是否存在会议室。它将 roomId 作为输入,并执行异步操作从数据库中获取数据。如果 roomId 无效或会议室不存在,它就会显示一条警告信息,提示用户等待指导员启动会议。否则,它会触发带有 screens.JOIN 参数的 onCallOrJoin 函数,大概是为了加入会议。
CallScreen.js
用户单击“开始会议”按钮后,他们将进入“呼叫屏幕”,该屏幕会自动建立与数据库的连接。
//CallScreen.js
import React, { useState, useEffect } from "react";
import { View } from "react-native";
import {
RTCPeerConnection,
RTCView,
mediaDevices,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
} from "react-native-webrtc";
import { db } from "../firebase";
import {
addDoc,
collection,
doc,
setDoc,
getDoc,
updateDoc,
onSnapshot,
deleteField,
} from "firebase/firestore";
import CallActionBox from "../components/CallActionBox";
const configuration = {
iceServers: [
{
urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"],
},
],
iceCandidatePoolSize: 10,
};
export default function CallScreen({ roomId, screens, setScreen }) {
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [cachedLocalPC, setCachedLocalPC] = useState();
const [isMuted, setIsMuted] = useState(false);
const [isOffCam, setIsOffCam] = useState(false);
useEffect(() => {
startLocalStream();
}, []);
useEffect(() => {
if (localStream && roomId) {
startCall(roomId);
}
}, [localStream, roomId]);
//End call button
async function endCall() {
if (cachedLocalPC) {
const senders = cachedLocalPC.getSenders();
senders.forEach((sender) => {
cachedLocalPC.removeTrack(sender);
});
cachedLocalPC.close();
}
const roomRef = doc(db, "room", roomId);
await updateDoc(roomRef, { answer: deleteField() });
setLocalStream();
setRemoteStream(); // set remoteStream to null or empty when callee leaves the call
setCachedLocalPC();
// cleanup
setScreen(screens.ROOM); //go back to room screen
}
//start local webcam on your device
const startLocalStream = async () => {
// isFront will determine if the initial camera should face user or environment
const isFront = true;
const devices = await mediaDevices.enumerateDevices();
const facing = isFront ? "front" : "environment";
const videoSourceId = devices.find(
(device) => device.kind === "videoinput" && device.facing === facing
);
const facingMode = isFront ? "user" : "environment";
const constraints = {
audio: true,
video: {
mandatory: {
minWidth: 500, // Provide your own width, height and frame rate here
minHeight: 300,
minFrameRate: 30,
},
facingMode,
optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
},
};
const newStream = await mediaDevices.getUserMedia(constraints);
setLocalStream(newStream);
};
const startCall = async (id) => {
const localPC = new RTCPeerConnection(configuration);
localStream.getTracks().forEach((track) => {
localPC.addTrack(track, localStream);
});
const roomRef = doc(db, "room", id);
const callerCandidatesCollection = collection(roomRef, "callerCandidates");
const calleeCandidatesCollection = collection(roomRef, "calleeCandidates");
localPC.addEventListener("icecandidate", (e) => {
if (!e.candidate) {
console.log("Got final candidate!");
return;
}
addDoc(callerCandidatesCollection, e.candidate.toJSON());
});
localPC.ontrack = (e) => {
const newStream = new MediaStream();
e.streams[0].getTracks().forEach((track) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = await localPC.createOffer();
await localPC.setLocalDescription(offer);
await setDoc(roomRef, { offer, connected: false }, { merge: true });
// Listen for remote answer
onSnapshot(roomRef, (doc) => {
const data = doc.data();
if (!localPC.currentRemoteDescription && data.answer) {
const rtcSessionDescription = new RTCSessionDescription(data.answer);
localPC.setRemoteDescription(rtcSessionDescription);
} else {
setRemoteStream();
}
});
// when answered, add candidate to peer connection
onSnapshot(calleeCandidatesCollection, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
let data = change.doc.data();
localPC.addIceCandidate(new RTCIceCandidate(data));
}
});
});
setCachedLocalPC(localPC);
};
const switchCamera = () => {
localStream.getVideoTracks().forEach((track) => track._switchCamera());
};
// Mutes the local's outgoing audio
const toggleMute = () => {
if (!remoteStream) {
return;
}
localStream.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsMuted(!track.enabled);
});
};
const toggleCamera = () => {
localStream.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsOffCam(!isOffCam);
});
};
return (
<View className="flex-1 bg-red-600">
{!remoteStream && (
<RTCView
className="flex-1"
streamURL={localStream && localStream.toURL()}
objectFit={"cover"}
/>
)}
{remoteStream && (
<>
<RTCView
className="flex-1"
streamURL={remoteStream && remoteStream.toURL()}
objectFit={"cover"}
/>
{!isOffCam && (
<RTCView
className="w-32 h-48 absolute right-6 top-8"
streamURL={localStream && localStream.toURL()}
/>
)}
</>
)}
<View className="absolute bottom-0 w-full">
<CallActionBox
switchCamera={switchCamera}
toggleMute={toggleMute}
toggleCamera={toggleCamera}
endCall={endCall}
/>
</View>
</View>
);
}
首先,我们需要从我们将要使用的 WebRTC 和 Firebase 库中导入函数。然后,我们创建一个变量configuration
来存储 ICE 服务器。
WebRTC 中的 ICE 服务器可帮助防火墙和 NAT 后面的设备发现彼此的网络地址,通过使用 STUN 进行直接连接并在无法进行直接通信时使用 TURN 作为后备中继来实现高效的点对点通信。
在第一次useEffect()
呼叫中,我们调用startLocalStream()
函数来自动在呼叫者的设备上启动本地视频流。
在第二次useEffect()
调用中,我们确保有一个roomId
,并且localStream
不为空。
该endCall()
函数执行以下操作:
cachedLocalPC
它检查在呼叫期间是否建立了缓存的本地对等连接 ( )。如果存在本地对等连接,它将删除与其关联的所有轨道,然后关闭该连接。- 接下来,它通过设置要删除的“答案”字段来更新数据库中的“房间”文档,从而有效地清除房间中的答案信息。
- 然后,该函数会重置
localStream
和remoteStream
,确保它们为空或设置为 null,特别是对于“被调用者”在离开呼叫时。 - 清理资源后,缓存的本地对等连接 (
cachedLocalPC
) 被重置。 - 最后,
setScreen()
调用该函数导航回房间屏幕,结束通话并将用户返回到原始状态。
startLocalStream()
函数负责从用户设备摄像头捕获本地视频流。以下是其工作原理的简要说明:
- 它枚举有关可用视频源的可用媒体设备。
- 该函数定义视频流的约束,例如最小宽度、最小高度和最小帧速率。
getUserMedia
使用定义的约束调用该函数,提示用户授予摄像机访问权限并返回本地视频流。- 最后,将获得的视频流设置为
localStream
,以便在应用程序中进一步使用。
startCall()
函数通过执行以下步骤发起调用:
localPC
创建一个新的本地 PeerConnection ( )。localStream
添加本地视频流 ( )localPC
以在通话期间启用本地媒体共享。- 该函数建立与 Firestore 数据库的连接以处理与呼叫相关的数据。
- 将事件侦听器添加到
localPC
处理 ICE 候选事件,并将任何生成的候选事件添加到“callerCandidates”的 Firestore 集合中。 - 该函数侦听
ontrack
上的事件localPC
,该事件表明远程对等点已开始流式传输其媒体。然后它创建一个新的MediaStream
来存储传入流并将其设置为remoteStream
. createOffer
使用的方法创建报价localPC
,并相应地设置本地描述。- 使用该方法将优惠以及设置为 的“已连接”状态
false
存储在 Firestore 房间文档中setDoc
。 - 该函数侦听 Firestore 房间文档中的任何更新(例如,当远程对等方提供答案时)。
- 如果从远程对等点收到应答,则会对其进行处理,并在对等点之间设置远程描述以
localPC
建立对等点之间的连接。 - 此外,被调用者添加的任何 ICE 候选者都会从“calleeCandidates”的 Firestore 集合中检索,并添加到
localPC
. - 最后,
localPC
使用 存储在缓存的状态变量中setCachedLocalPC
。
switchCamera
功能允许用户在视频通话期间在前置摄像头和后置摄像头之间切换
toggleMute
功能允许用户在视频通话期间静音和取消静音。
toggleCamera
功能允许用户在视频通话期间打开和关闭摄像头。
JoinScreen.js
如果用户单击“加入会议”按钮,如果他们具有相同的房间 ID,系统会自动将他们连接到呼叫者的会议。如果被叫方尝试加入的房间ID不存在,则会提示输入的房间ID不存在。
//JoinScreen.js
import React, { useState, useEffect } from "react";
import { Text, StyleSheet, Button, View } from "react-native";
import {
RTCPeerConnection,
RTCView,
mediaDevices,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
} from "react-native-webrtc";
import { db } from "../firebase";
import {
addDoc,
collection,
doc,
setDoc,
getDoc,
updateDoc,
onSnapshot,
deleteField,
} from "firebase/firestore";
import CallActionBox from "../components/CallActionBox";
const configuration = {
iceServers: [
{
urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"],
},
],
iceCandidatePoolSize: 10,
};
export default function JoinScreen({ roomId, screens, setScreen }) {
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [cachedLocalPC, setCachedLocalPC] = useState();
const [isMuted, setIsMuted] = useState(false);
const [isOffCam, setIsOffCam] = useState(false);
//Automatically start stream
useEffect(() => {
startLocalStream();
}, []);
useEffect(() => {
if (localStream) {
joinCall(roomId);
}
}, [localStream]);
//End call button
async function endCall() {
if (cachedLocalPC) {
const senders = cachedLocalPC.getSenders();
senders.forEach((sender) => {
cachedLocalPC.removeTrack(sender);
});
cachedLocalPC.close();
}
const roomRef = doc(db, "room", roomId);
await updateDoc(roomRef, { answer: deleteField(), connected: false });
setLocalStream();
setRemoteStream(); // set remoteStream to null or empty when callee leaves the call
setCachedLocalPC();
// cleanup
setScreen(screens.ROOM); //go back to room screen
}
//start local webcam on your device
const startLocalStream = async () => {
// isFront will determine if the initial camera should face user or environment
const isFront = true;
const devices = await mediaDevices.enumerateDevices();
const facing = isFront ? "front" : "environment";
const videoSourceId = devices.find(
(device) => device.kind === "videoinput" && device.facing === facing
);
const facingMode = isFront ? "user" : "environment";
const constraints = {
audio: true,
video: {
mandatory: {
minWidth: 500, // Provide your own width, height and frame rate here
minHeight: 300,
minFrameRate: 30,
},
facingMode,
optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
},
};
const newStream = await mediaDevices.getUserMedia(constraints);
setLocalStream(newStream);
};
//join call function
const joinCall = async (id) => {
const roomRef = doc(db, "room", id);
const roomSnapshot = await getDoc(roomRef);
if (!roomSnapshot.exists) return;
const localPC = new RTCPeerConnection(configuration);
localStream.getTracks().forEach((track) => {
localPC.addTrack(track, localStream);
});
const callerCandidatesCollection = collection(roomRef, "callerCandidates");
const calleeCandidatesCollection = collection(roomRef, "calleeCandidates");
localPC.addEventListener("icecandidate", (e) => {
if (!e.candidate) {
console.log("Got final candidate!");
return;
}
addDoc(calleeCandidatesCollection, e.candidate.toJSON());
});
localPC.ontrack = (e) => {
const newStream = new MediaStream();
e.streams[0].getTracks().forEach((track) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = roomSnapshot.data().offer;
await localPC.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await localPC.createAnswer();
await localPC.setLocalDescription(answer);
await updateDoc(roomRef, { answer, connected: true }, { merge: true });
onSnapshot(callerCandidatesCollection, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
let data = change.doc.data();
localPC.addIceCandidate(new RTCIceCandidate(data));
}
});
});
onSnapshot(roomRef, (doc) => {
const data = doc.data();
if (!data.answer) {
setScreen(screens.ROOM);
}
});
setCachedLocalPC(localPC);
};
const switchCamera = () => {
localStream.getVideoTracks().forEach((track) => track._switchCamera());
};
// Mutes the local's outgoing audio
const toggleMute = () => {
if (!remoteStream) {
return;
}
localStream.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsMuted(!track.enabled);
});
};
const toggleCamera = () => {
localStream.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsOffCam(!isOffCam);
});
};
return (
<View className="flex-1">
<RTCView
className="flex-1"
streamURL={remoteStream && remoteStream.toURL()}
objectFit={"cover"}
/>
{remoteStream && !isOffCam && (
<RTCView
className="w-32 h-48 absolute right-6 top-8"
streamURL={localStream && localStream.toURL()}
/>
)}
<View className="absolute bottom-0 w-full">
<CallActionBox
switchCamera={switchCamera}
toggleMute={toggleMute}
toggleCamera={toggleCamera}
endCall={endCall}
/>
</View>
</View>
);
}
正如你所看到的,joinCall
和startCall
几乎相同,但用途和功能有所不同。
joinCall
功能用于加入由呼叫者发起的现有呼叫。
- 它使用
roomSnapshot.exists
来检查数据库 (Firestore) 中是否存在提供的房间 ID 。 - 如果房间ID不存在(
!roomSnapshot.exists
),则返回并且不继续连接。 - 如果房间 ID 存在,该函数会建立本地对等连接 (
localPC
) 并向其中添加本地视频流。 - 它为 ICE 候选者设置事件侦听器以交换信令数据。
- 收集 ICE 候选者后,它们将被添加到“calleeCandidates”的 Firestore 集合中。
- 该函数侦听 Firestore 房间文档中的更改并处理从调用者收到的远程报价。
- 处理报价后,它会创建并设置本地答案并更新 Firestore 房间文档以指示连接成功。
- 该函数设置使用
setRemoteStream
函数从调用者接收的远程流。 - 然后将其
localPC
缓存以供setCachedLocalPC
函数进一步使用。
构建APK
使用 EAS Build 构建 Android 应用程序时使用的默认文件格式是Android App Bundle (AAB/.aab)。此格式针对分发到 Google Play 商店进行了优化。但是,AAB 无法直接安装在您的设备上。要将构建直接安装到 Android 设备或模拟器,您需要构建Android 包(APK/.apk)。您可以按照此链接上的说明来指导您如何将其制作为 apk 文件。
完整代码链接:https://github.com/KyleeMendoza/firebaseWebrtc
作者:Kyle Mendoza
本文为原创稿件,版权归作者所有,如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/30054.html