使用 React Native 构建实时视频聊天应用
Daily 的React Native 库允许开发者使用同一套代码库构建同时兼容 Android 和 iOS 的移动应用。这也意味着,您的 Web 开发人员(他们很可能之前接触过React)可以编写能够编译成原生移动代码的代码,而且学习曲线更平缓,因为 React 和 React Native 非常相似。
最近,我们在 Daily 博客上讨论了如何使用 React Native构建自己的音频通话应用。该教程专门介绍了 Daily 的Party Line 演示应用,该应用旨在处理所有通话均为纯音频通话的场景。
今日议程
在今天的教程中,我们将了解 Daily 的 React Native Playground 演示应用程序,该应用程序采用更传统的视频通话格式;通话参与者可以选择打开或关闭音频和视频。
更具体地说,我们将涵盖以下内容:
- 如何使用 React Native 构建多人视频通话
react-native-daily-js - 如何在视频通话中赋予通话参与者对其设备的控制权,以便他们切换本地麦克风和摄像头?
本教程面向哪些用户?
如果您对开发一款支持视频通话的移动应用感兴趣,并且具备一定的 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'
}
/>
// 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);
});
};
在这里,我们将appState状态值更新为临时的“创建”状态,调用api.createRoom(),如果成功,则设置我们的roomUrlFieldValue值和appState。(appState和roomUrlFieldValue都是在中初始化的组件状态值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}
/>
接下来,我们调用startCall:
// App.tsx
/**
* Join the room provided by the user or the
* temporary room created by createRoom
*/
const startCall = () => {
setRoomUrl(roomUrlFieldValue);
};
最后,当值更新时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]);
下面这行代码实际创建了调用对象: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]);
这里,在另一个useEffect钩子中App,当callObject和roomUrl状态值为真时(现在它们就是真值),我们可以通过将传递给我们的调用对象实例来实际join调用。roomUrl
这一步也是应用视图从主屏幕切换到通话视图的时刻。这是由于上面效果中的这一行代码造成的:setAppState(AppState.Joining);
// App.tsx
const showCallPanel = [
AppState.Joining,
AppState.Joined,
AppState.Error,
].includes(appState);
当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
)
...
主屏幕部分就先讲到这里,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>
注意:我们将其放在一个组件中,ScrollView但FlatList如果您知道会有较大规模的通话,您可能更倾向于使用组件。(组件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]);
让我们逐步分析这个函数:
- 我们声明了两个数组,我们将在本函数中更新它们
larges:thumbnails - 我们获取通话参与者数组(
Object.entries(callState.callItems)),并对每个参与者执行以下操作(或者forEach,如果你愿意的话):- 注:
tileType可以是TileType.Full、TileType.Half或TileType.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]);
和是传递给 的属性,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;
}
我们这里有点跳跃,但需要理解的关键是,我们的 DailycallObject会提供参与者信息(参见:)callObject.participants(),而参与者信息包含他们的媒体(视频/音频)文件。然后,我们可以将这些文件传递给组件DailyMediaView,以便在应用程序中实际播放它们。
回到组件Tile,我们从 ` and` props 中获取videoTrack`and`audioTrack值。videoTrackStateaudioTrackState
// 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]);
这意味着,如果参与者个人信息中包含曲目信息,我们将使用该信息;否则,我们将相应的属性设置为。和属性null都属于有效类型。DailyMediaView videoTrackaudioTrack
Tile当音频和摄像头处于静音状态时(例如没有播放曲目时),还会显示一个带有静音图标的叠加层,但我们不会在此处审查该代码。如有任何疑问,请随时联系我们。🙏
通话中控制本地设备
最后,我们来看看Tray组件是如何与每日呼叫对象交互的。需要提醒的是,每日呼叫对象与组件是App.tsx同时CallPanel渲染的。
如前所述,该托盘可以控制本地摄像头和麦克风,以及退出当前通话返回主屏幕。
要切换本地摄像头,我们可以调用setLocalAudiocall 对象实例。
// Tray.tsx
const toggleCamera = useCallback(() => {
callObject?.setLocalVideo(isCameraMuted);
}, [callObject, isCameraMuted]);
同样,我们可以使用 来打开或关闭麦克风setLocalAudio。
// Tray.tsx
const toggleMic = useCallback(() => {
callObject?.setLocalAudio(isMicMuted);
}, [callObject, isMicMuted]);
最后,按下“离开”按钮将调用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]);
在这里,我们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



