如何使用 WebRTC 和 Firebase 在 React Native 上实现视频通话应用程序

欢迎阅读本综合指南,了解如何使用 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()函数执行以下操作:

  1. cachedLocalPC它检查在呼叫期间是否建立了缓存的本地对等连接 ( )。如果存在本地对等连接,它将删除与其关联的所有轨道,然后关闭该连接。
  2. 接下来,它通过设置要删除的“答案”字段来更新数据库中的“房间”文档,从而有效地清除房间中的答案信息。
  3. 然后,该函数会重置localStreamremoteStream,确保它们为空或设置为 null,特别是对于“被调用者”在离开呼叫时。
  4. 清理资源后,缓存的本地对等连接 ( cachedLocalPC) 被重置。
  5. 最后,setScreen()调用该函数导航回房间屏幕,结束通话并将用户返回到原始状态。

startLocalStream()函数负责从用户设备摄像头捕获本地视频流。以下是其工作原理的简要说明:

  1. 它枚举有关可用视频源的可用媒体设备。
  2. 该函数定义视频流的约束,例如最小宽度、最小高度和最小帧速率。
  3. getUserMedia使用定义的约束调用该函数,提示用户授予摄像机访问权限并返回本地视频流。
  4. 最后,将获得的视频流设置为localStream,以便在应用程序中进一步使用。

startCall()函数通过执行以下步骤发起调用:

  1. localPC创建一个新的本地 PeerConnection ( )。
  2. localStream添加本地视频流 ( )localPC以在通话期间启用本地媒体共享。
  3. 该函数建立与 Firestore 数据库的连接以处理与呼叫相关的数据。
  4. 将事件侦听器添加到localPC处理 ICE 候选事件,并将任何生成的候选事件添加到“callerCandidates”的 Firestore 集合中。
  5. 该函数侦听ontrack上的事件localPC,该事件表明远程对等点已开始流式传输其媒体。然后它创建一个新的MediaStream来存储传入流并将其设置为remoteStream.
  6. createOffer使用的方法创建报价localPC,并相应地设置本地描述。
  7. 使用该方法将优惠以及设置为 的“已连接”状态false存储在 Firestore 房间文档中setDoc
  8. 该函数侦听 Firestore 房间文档中的任何更新(例如,当远程对等方提供答案时)。
  9. 如果从远程对等点收到应答,则会对其进行处理,并在对等点之间设置远程描述以localPC建立对等点之间的连接。
  10. 此外,被调用者添加的任何 ICE 候选者都会从“calleeCandidates”的 Firestore 集合中检索,并添加到localPC.
  11. 最后,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>
  );
}

正如你所看到的,joinCallstartCall几乎相同,但用途和功能有所不同。

joinCall功能用于加入由呼叫者发起的现有呼叫。

  1. 它使用 roomSnapshot.exists 来检查数据库 (Firestore) 中是否存在提供的房间 ID 。
  2. 如果房间ID不存在( !roomSnapshot.exists),则返回并且不继续连接。
  3. 如果房间 ID 存在,该函数会建立本地对等连接 ( localPC) 并向其中添加本地视频流。
  4. 它为 ICE 候选者设置事件侦听器以交换信令数据。
  5. 收集 ICE 候选者后,它们将被添加到“calleeCandidates”的 Firestore 集合中。
  6. 该函数侦听 Firestore 房间文档中的更改并处理从调用者收到的远程报价。
  7. 处理报价后,它会创建并设置本地答案并更新 Firestore 房间文档以指示连接成功。
  8. 该函数设置使用setRemoteStream函数从调用者接收的远程流。
  9. 然后将其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

(0)

相关推荐

发表回复

登录后才能评论