发布于 2026-01-06 1 阅读
0

使用 React Native 构建实时视频聊天应用

使用 React Native 构建实时视频聊天应用

Daily 的React Native 库允许开发者使用同一套代码库构建同时兼容 Android 和 iOS 的移动应用。这也意味着,您的 Web 开发人员(他们很可能之前接触过React)可以编写能够编译成原生移动代码的代码,而且学习曲线更平缓,因为 React 和 React Native 非常相似。

最近,我们在 Daily 博客上讨论了如何使用 React Native构建自己的音频通话应用。该教程专门介绍了 Daily 的Party Line 演示应用,该应用旨在处理所有通话均为纯音频通话的场景。

今日议程

在今天的教程中,我们将了解 Daily 的 React Native Playground 演示应用程序,该应用程序采用更传统的视频通话格式;通话参与者可以选择打开或关闭音频和视频。

更具体地说,我们将涵盖以下内容:

  1. 如何使用 React Native 构建多人视频通话react-native-daily-js
  2. 如何在视频通话中赋予通话参与者对其设备的控制权,以便他们切换本地麦克风和摄像头?

本教程面向哪些用户?

如果您对开发一款支持视频通话的移动应用感兴趣,并且具备一定的 React Native(甚至 React)知识,那么本教程非常适合您。由于平台特定的配置要求,React Native 项目在本地运行可能比 Web 应用更繁琐一些,因此熟悉 React Native 将大有裨益。

本教程不会涵盖Playground 演示应用程序代码库的每个部分,因为很多功能与daily-js(Web)应用程序类似,而我们保证已经有很多关于 Web 应用程序的教程可供参考。📚

关于今天技术栈和 React Hooks 的一点说明

由于此应用是用 React Native 编写的,我们将参考演示代码库中的 React Native 代码示例和 React Hooks。为了更好地理解本教程,我们建议您在继续阅读之前先熟悉React Hooks 。

在这个演示应用中,我们也使用了TypeScript。虽然下文不会专门讨论 TypeScript,但对它有一定的了解将有助于你理解代码示例。


入门

对于任何刚接触 React Native 应用开发的人来说,我们将快速介绍一些基础知识。

通常情况下,你需要同时在安卓和iOS手机或平板电脑上进行测试,以确保你的应用在两个操作系统上都能正常运行。要在iOS设备上进行测试,你需要下载Xcode,而Xcode目前只能在Mac电脑上下载。(下载的时候,你最好给自己倒杯咖啡,然后祈祷自己没有赶工期。☕)

注意:这意味着您需要一台Mac电脑才能访问Xcode进行iOS开发。

不过,Android 可以使用Android Studio进行测试,该软件可在多种操作系统上运行。

关于如何在本地运行Daily Playground 演示应用程序,仓库的 README 文件中包含了 Android 和 iOS 开发的详细说明

注意:测试视频/音频功能时,需要使用真机而非设备模拟器。至于选择哪个操作系统,如果您没有个人偏好,通常情况下,在安卓设备上运行此应用会更快。


应用功能

如前所述,我们不会讲解代码库的每个部分。首先,让我们讨论一下应用程序的整体结构和功能,以便您了解如何使用它。

App组件是顶级父组件。它负责渲染主屏幕或通话中视图。

两种应用视图:主屏幕和通话中视图

让我们快速回顾一下主屏幕的工作原理。

当你第一次进入主屏幕时,你会看到一个空白的房间 URL 文本输入框、一个“创建演示房间”按钮和一个禁用的“加入通话”按钮。

如果您知道要加入哪个每日聊天室,可以在文本输入框中输入聊天室 URL,然后按“加入通话”按钮,该按钮会在输入框有值后启用。

如果您没有房间 URL,我们已设置一个端点,可以使用Daily 的 REST API为您创建一个新房间。当您按下“创建房间”按钮时,将调用此端点,该端点会调用createRoom以下定义的方法App

// App.tsx
<Button
    type="secondary"
    onPress={createRoom}
    label={
       appState === AppState.Creating
          ? 'Creating room...'
          : 'Create demo room'
    }
/>
Enter fullscreen mode Exit fullscreen mode
// App.tsx
 const createRoom = () => {
   setRoomCreateError(false);
   setAppState(AppState.Creating);
   api
     .createRoom()
     .then((room) => {
       setRoomUrlFieldValue(room.url);
       setAppState(AppState.Idle);
     })
     .catch(() => {
       setRoomCreateError(true);
       setRoomUrlFieldValue(undefined);
       setAppState(AppState.Idle);
     });
 };
Enter fullscreen mode Exit fullscreen mode

在这里,我们将appState状态值更新为临时的“创建”状态,调用api.createRoom(),如果成功,则设置我们的roomUrlFieldValue值和appState。(appStateroomUrlFieldValue都是在中初始化的组件状态值App。)

注意:请查看api.ts文件以了解该api.createRoom()方法。

无论你使用自己的每日房间 URL 还是在应用程序中创建的房间 URL,当你按下“加入通话”按钮时,它将获取该 URL roomUrlFieldValue,用它设置roomUrl状态值,并开始创建每日通话对象

这里是“加入通话”按钮:

// App.tsx
// “Join call” button will call startCall on press
<StartButton
   onPress={startCall}
   disabled={startButtonDisabled}
   starting={appState === AppState.Joining}
/>
Enter fullscreen mode Exit fullscreen mode

接下来,我们调用startCall

// App.tsx
/**
  * Join the room provided by the user or the
  * temporary room created by createRoom
 */
 const startCall = () => {
   setRoomUrl(roomUrlFieldValue);
 };
Enter fullscreen mode Exit fullscreen mode

最后,当值更新时useEffect会触发一个钩子roomURL,该钩子会创建我们的每日调用对象(这是整个操作的核心!)。

// App.tsx
/**
  * Create the callObject as soon as we have a roomUrl.
  * This will trigger the call starting.
  */
 useEffect(() => {
   if (!roomUrl) {
     return;
   }
   const newCallObject = Daily.createCallObject();
   setCallObject(newCallObject);
 }, [roomUrl]);

Enter fullscreen mode Exit fullscreen mode

下面这行代码实际创建了调用对象:
const newCallObject = Daily.createCallObject();

然后,通过将该值设置到组件的状态中,以后就可以引用调用对象实例了:

setCallObject(newCallObject);

创建通话对象后,我们就可以真正加入房间了(终于!毕竟我们按下了“加入通话”按钮😉)。

// App.tsx
 useEffect(() => {
   if (!callObject || !roomUrl) {
     return;
   }
   callObject.join({ url: roomUrl }).catch((_) => {
     // Doing nothing here since we handle fatal join errors in another way,
     // via our listener attached to the 'error' event
   });
   setAppState(AppState.Joining);
 }, [callObject, roomUrl]);
Enter fullscreen mode Exit fullscreen mode

这里,在另一个useEffect钩子中App,当callObjectroomUrl状态值为真时(现在它们就是真值),我们可以通过将传递给我们的调用对象实例来实际join调用。roomUrl

这一步也是应用视图从主屏幕切换到通话视图的时刻。这是由于上面效果中的这一行代码造成的:setAppState(AppState.Joining);

// App.tsx
 const showCallPanel = [
   AppState.Joining,
   AppState.Joined,
   AppState.Error,
 ].includes(appState);

Enter fullscreen mode Exit fullscreen mode

showCallPanel(如上所示)为真时,将渲染通话中视图而不是主屏幕:

// App.tsx
<View style={styles.container}>
    {showCallPanel ? (
         <View style={[
             styles.callContainerBase,
                orientation === Orientation.Landscape
                    ? styles.callContainerLandscape
                    : null,
         ]}>
             <CallPanel roomUrl={roomUrl || ''} />
             <Tray
                onClickLeaveCall={leaveCall}
                disabled={!enableCallButtons}
             />
        </View>
    ) : (
     //home screen
    )
...
Enter fullscreen mode Exit fullscreen mode

主屏幕部分就先讲到这里,CallPanel接下来我们将重点讲解通话中视图组件。如果您对这部分有任何疑问,欢迎随时联系我们!我们很乐意为您提供帮助。🙌


在您的 Daily React Native 应用中显示视频图块

首先,让我们熟悉一下通话应用程序的用户界面应该是什么样子:

通话中用户界面,一位开发者自言自语

屏幕左上角显示本地参与者的摄像头画面,屏幕中间显示房间网址和一个复制到剪贴板的按钮,底部是我们的工具栏。如果有人正在共享屏幕,他们的头像也会显示在屏幕顶部。

注意:此应用程序无法发起屏幕共享,但通话参与者可以从任何平台(包括允许屏幕共享daily-js的 Web 应用程序)加入房间。

托盘(即Tray组件)上有按钮可以切换本地参与者的音频、视频以及离开通话。

当更多参与者加入时,他们的视频将显示在屏幕中央,取代房间 URL 信息。

遍历我们的参与者列表

既然我们已经了解了我们要讨论的内容,那就让我们直接进入实际创建参与者视频的部分吧react-native-daily-js

在 中CallPanel.tsx,我们渲染一个名为 的数组largeTiles,它表示远程参与者。

// CallPanel.tsx
<ScrollView
     alwaysBounceVertical={false}
     alwaysBounceHorizontal={false}
     horizontal={orientation === Orientation.Landscape}
 >
     <View
        style={[
            styles.largeTilesContainerInnerBase,
                orientation === Orientation.Portrait
                 ? styles.largeTilesContainerInnerPortrait
                 : styles.largeTilesContainerInnerLandscape,
         ]}
      >
         {largeTiles} // <- our remote participants
      </View>
 </ScrollView>
Enter fullscreen mode Exit fullscreen mode

注意:我们将其放在一个组件中,ScrollViewFlatList如果您知道会有较大规模的通话,您可能更倾向于使用组件。(组件FlatList只会渲染可见的图块,这有助于提高性能。在1对1视频通话中,这方面的影响较小。)

我们的largeTiles(远程参与者)和(本地参与者或屏幕共享者)由同一个记忆thumbnailTiles函数决定。图块的大小可以根据参与者人数而定,可以是全尺寸或半尺寸。largeTiles

全尺寸和半尺寸远程参与者图块

// CallPanel.tsx
 /**
  * Get lists of large tiles and thumbnail tiles to render.
  */
 const [largeTiles, thumbnailTiles] = useMemo(() => {
   let larges: JSX.Element[] = [];
   let thumbnails: JSX.Element[] = [];
   Object.entries(callState.callItems).forEach(([id, callItem]) => {
     let tileType: TileType;
     if (isScreenShare(id)) {
       tileType = TileType.Full;
     } else if (isLocal(id) || containsScreenShare(callState.callItems)) {
       tileType = TileType.Thumbnail;
     } else if (participantCount(callState.callItems) <= 3) {
       tileType = TileType.Full;
     } else {
       tileType = TileType.Half;
     }
     const tile = (
       <Tile
         key={id}
         videoTrackState={callItem.videoTrackState}
         audioTrackState={callItem.audioTrackState}
         mirror={usingFrontCamera && isLocal(id)}
         type={tileType}
         disableAudioIndicators={isScreenShare(id)}
         onPress={
           isLocal(id)
             ? flipCamera
             : () => {
                 sendHello(id);
               }
         }
       />
     );
     if (tileType === TileType.Thumbnail) {
       thumbnails.push(tile);
     } else {
       larges.push(tile);
     }
   });
   return [larges, thumbnails];
 }, [callState.callItems, flipCamera, sendHello, usingFrontCamera]);

Enter fullscreen mode Exit fullscreen mode

让我们逐步分析这个函数:

  • 我们声明了两个数组,我们将在本函数中更新它们largesthumbnails
  • 我们获取通话参与者数组(Object.entries(callState.callItems)),并对每个参与者执行以下操作(或者forEach,如果你愿意的话):
    • 注:tileType可以是TileType.FullTileType.HalfTileType.Thumbnail。后者是本地参与者,前两个选项是远程参与者(我们的largeTiles)。
    • 如果“参与者”实际上是屏幕共享,我们会将其显示为全尺寸图块。
    • 如果参与者是本地用户或当前正在共享屏幕,我们会为其生成缩略图。
    • 如果通话参与者总数为 3 人或以下,远程参与者将拥有全尺寸图块;否则,他们将拥有半尺寸图块。
    • 然后,我们Tile为每个参与者渲染一个组件,并更新我们的larges数组thumbnails

好的,我们已经取得了很大进展,但我们仍然需要为参与者渲染实际的视频和音频,请耐心等待!

呈现参与者媒体

组件最重要的部分Tile是,它是从以下位置导入的组件mediaComponent的记忆化实例DailyMediaViewreact-native-daily-js

// Tile.tsx
import {
   DailyMediaView,
} from '@daily-co/react-native-daily-js';
... 
const mediaComponent = useMemo(() => {
   return (
     <DailyMediaView
       videoTrack={videoTrack}
       audioTrack={audioTrack}
       mirror={props.mirror}
       zOrder={props.type === TileType.Thumbnail ? 1 : 0}
       style={styles.media}
       objectFit="cover"
     />
   );
 }, [videoTrack, audioTrack, props.mirror, props.type]);
Enter fullscreen mode Exit fullscreen mode

是传递给 的属性videoTrack实际上是在 中设置的audioTrackTileCallPanelcallState.ts

// callState.ts
function getCallItems(participants: { [id: string]: DailyParticipant }) {
 // Ensure we *always* have a local participant
 let callItems = { ...initialCallState.callItems }; 
 for (const [id, participant] of Object.entries(participants)) {
   callItems[id] = {
     videoTrackState: participant.tracks.video,
     audioTrackState: participant.tracks.audio,
   };
   if (shouldIncludeScreenCallItem(participant)) {
     callItems[id + '-screen'] = {
       videoTrackState: participant.tracks.screenVideo,
       audioTrackState: participant.tracks.screenAudio,
     };
   }
 }
 return callItems;
}

Enter fullscreen mode Exit fullscreen mode

我们这里有点跳跃,但需要理解的关键是,我们的 DailycallObject会提供参与者信息(参见:)callObject.participants(),而参与者信息包含他们的媒体(视频/音频)文件。然后,我们可以将这些文件传递给组件DailyMediaView,以便在应用程序中实际播放它们。

回到组件Tile,我们从 ` and` props 中获取videoTrack`and`audioTrackvideoTrackStateaudioTrackState

// Tile.tsx
 const videoTrack = useMemo(() => {
   return props.videoTrackState
      && props.videoTrackState.state === 'playable'
     ? props.videoTrackState.track!
     : null;
 }, [props.videoTrackState]);

 const audioTrack = useMemo(() => {
   return props.audioTrackState && props.audioTrackState.state === 'playable'
     ? props.audioTrackState.track!
     : null;
 }, [props.audioTrackState]);
Enter fullscreen mode Exit fullscreen mode

这意味着,如果参与者个人信息中包含曲目信息,我们将使用该信息;否则,我们将相应的属性设置为。和属性null都属于有效类型DailyMediaView videoTrackaudioTrack

Tile当音频和摄像头处于静音状态时(例如没有播放曲目时),还会显示一个带有静音图标的叠加层,但我们不会在此处审查该代码。如有任何疑问,请随时联系我们。🙏

图块图标叠加


通话中控制本地设备

最后,我们来看看Tray组件是如何与每日呼叫对象交互的。需要提醒的是,每日呼叫对象与组件是App.tsx同时CallPanel渲染的。

Android 托盘

如前所述,该托盘可以控制本地摄像头和麦克风,以及退出当前通话返回主屏幕。

要切换本地摄像头,我们可以调用setLocalAudiocall 对象实例。

// Tray.tsx
 const toggleCamera = useCallback(() => {
   callObject?.setLocalVideo(isCameraMuted);
 }, [callObject, isCameraMuted]);

Enter fullscreen mode Exit fullscreen mode

同样,我们可以使用 来打开或关闭麦克风setLocalAudio

// Tray.tsx
 const toggleMic = useCallback(() => {
   callObject?.setLocalAudio(isMicMuted);
 }, [callObject, isMicMuted]);
Enter fullscreen mode Exit fullscreen mode

最后,按下“离开”按钮将调用leaveCall函数,该函数是通过传递的 prop传递的App

// App.tsx
/**
  * Leave the current call.
  * If we're in the error state (AppState.Error),
  * we've already "left", so just
  * clean up our state.
  */
 const leaveCall = useCallback(() => {
   if (!callObject) {
     return;
   }
   if (appState === AppState.Error) {
     callObject.destroy().then(() => {
       setRoomUrl(undefined);
       setRoomUrlFieldValue(undefined);
       setCallObject(null);
       setAppState(AppState.Idle);
     });
   } else {
     setAppState(AppState.Leaving);
     callObject.leave();
   }
 }, [callObject, appState]);

Enter fullscreen mode Exit fullscreen mode

在这里,我们destroy调用了我们的调用对象实例,并将状态重置App为初始值。


资源

我们希望这篇文章能帮助您使用 Daily 的 React Native 库构建自己的视频通话应用。我们涵盖了Playground 应用最重要的几个方面,但我们随时乐意解答您的任何疑问!😁

如果您想了解更多关于使用 Daily 的 React Native 库进行构建的信息,请查看我们精心编写的文档,或者阅读我们之前关于构建 Clubhouse 克隆应用的教程。📱

文章来源:https://dev.to/trydaily/build-a-real-time-video-chat-app-with-react-native-2hm