用声网Agora RTC(Real-time Messaging,云信令)SDK 在 React Native 上创建视频聊天应用非常简单。声网Agora RTM SDK 可以让多位用户通过同一频道名加入视频聊天室进行交流。
假设你正在创建一个社交视频聊天应用,你想让你的用户创建一个允许其他用户浏览、加入和说话的聊天室,可以使用后端服务器来处理这些请求,并向其他用户更新创建好的聊天室的信息,但是,这样做不仅需要编写后端代码,还需要托管服务器。
本教程将介绍一种使用声网Agora RTM SDK 实现这个目标的方法,我们将使用用户发送的消息来创建和更新动态视频聊天室,并且使用前端代码。
这样做的好处在于,当你不想搭建后端服务器时,可以用消息向其他用户更新聊天室状态。此方法也很容易扩展到完全的聊天室管理,以及管理员同意/拒绝用户、让用户静音、将用户从聊天室移除等功能。
在本文的示例中,我们用的是用于 React Native 框架的 Agora RTC SDK 和 Agora RTM SDK ,具体版本是 RTC SDK v3.2.2 和 RTM SDK v1.2.2-alpha.3。
项目概述
-
我们有一个名为“lobby”的 RTM 聊天室。当有人创建新聊天室或聊天室的成员发生变化时,我们会用“lobby”向用户发信号。
-
具体操作是,我们会让视频聊天室中的高级成员向其他成员发送消息。最早进入聊天室的成员就是高级成员(详见下文)。
-
我们会以 ‘roomName:memberCount’ 的形式发送消息,便于其他用户处理,把聊天室名称和成员数量存储在他们的应用状态中,然后用此形式渲染一个聊天室列表,列表中包含了聊天室成员的数量。
-
获得聊天室列表后,我们就可以通过使用 RTC SDK 加入聊天室。另外,还需监听用户加入/离开聊天室的状态,为其他用户更新成员数量。为控制管理成本,只有高级成员才可以这样操作。
-
为其他用户更新聊天室信息还需考虑两种情况:第一、当有新用户加入大厅时,每个频道中的高级成员会向该用户发送点对点消息。第二、当一个频道中的成员数量更新时,我们会给所有连接到大厅的用户发送频道消息,更新他们的聊天室列表。
创建一个声网Agora 账户
点击这里免费注册声网Agora 账户,登入后台。
网站上的 Project Management(项目管理)选项卡
找到 “Project Management” 选项卡下的 “Project List”选项卡,点击蓝色的“Create”按钮,创建一个项目。(当提示使用 App ID 和证书时,只选择 App ID)。App ID 可以在开发应用时对你的请求进行授权,不需要生成令牌,所以把 App ID 复制保存起来,方便以后使用。
注意: 本指南没有执行令牌验证,建议在生产环境中运行的所有 RTE 应用都采用令牌验证。如果想了解更多关于声网 Agora 平台基于令牌进行验证的信息,可以查看文档 校验用户权限。
下载源码
你可以直接跳转到代码,代码是开放源码,可以在 GitHub 上找到。如果想自己动手尝试,请先查看其中的 readme 文件了解运行应用所需步骤。
在安卓模拟器上运行该应用的截图
示例的结构
下面是我们正在搭建的应用的结构:
.
├── android
├── components
│ └── Permission.ts
│ └── Style.ts
├── ios
├── App.tsx
.
App.tsx
App.tsx 是应用的入口,所有代码都在这个文件中。
import React, {Component} from 'react';
import {
Platform,
SafeAreaView,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import RtcEngine, {
RtcRemoteView,
RtcLocalView,
VideoRenderMode,
} from 'react-native-agora';
import requestCameraAndAudioPermission from './components/Permission';
import styles from './components/Style';
import RtmEngine from 'agora-react-native-rtm';
interface Props {}
/**
* @property appId Agora App ID as string
* @property token Token for the channel
* @property channelName Channel Name for the current session
* @property inCall Boolean to store if we're in an active video chat room
* @property inLobby Boolean to store if we're in the lobby
* @property input String to store input
* @property peerIds Array for storing connected peers during a video chat
* @property seniors Array storing senior members in the joined channel
* @property myUsername Username to log in to RTM
* @property rooms Dictionary to store room names and their member count
*/
interface State {
appId: string;
token: string | null;
channelName: string;
inCall: boolean;
inLobby: boolean;
input: string;
peerIds: number[];
seniors: string[];
myUsername: string;
rooms: {[name: string]: number};
}
...
我们先编写输入声明,然后为应用状态定义一个接口,包含以下内容:
-
appId
:声网Agora App ID -
token
:为加入频道而生成的令牌 -
inCall
:存储我们是否在一个活跃的视频聊天室的布尔值 -
inLobby
:存储我们是否在大厅的布尔值 -
input
:创建新房间时存储输入的字符串 -
peerIdsRTC
:存储视频聊天室中其他用户的 RTC UID 的数组 -
seniors
: 存储在我们之前加入视频聊天室的 RTM 成员的数组 -
myUsername
:登录 RTM 的本地用户名称 -
rooms
:存储聊天室名称及成员数的字典
...
export default class App extends Component<null, State> {
_rtcEngine?: RtcEngine;
_rtmEngine?: RtmEngine;
constructor(props: any) {
super(props);
this.state = {
appId: '30a6bc89994d4222a71eba01c253cbc7',
token: null,
channelName: '',
inCall: false,
input: '',
inLobby: false,
peerIds: [],
seniors: [],
myUsername: '' + new Date().getTime(),
rooms: {},
};
if (Platform.OS === 'android') {
// Request required permissions from Android
requestCameraAndAudioPermission().then(() => {
console.log('requested!');
});
}
}
componentDidMount() {
// initialize the SDKs
this.initRTC();
this.initRTM();
}
componentWillUnmount() {
// destroy the engine instances
this._rtmEngine?.destroyClient();
this._rtcEngine?.destroy();
}
...
我们定义一个基于类的组件: _rtcEngine
变量存储 RtcEngine 类的实例,_rtmEngine
变量存储 RtmEngine 类的实例,我们可以通过这些实例访问 SDK 函数。
在构造函数中设置状态变量,并申请在安卓设备上录制音频的权限。(我们使用下面 permission.ts
中的辅助函数)。当组件被挂载时,我们调用 initRTC
和 initRTM
函数,用 App ID 初始化 RTC 和 RTM 引擎。当组件被卸载时,销毁引擎实例。
RTC初始化
...
/**
* @name initRTC
* @description Function to initialize the Rtc Engine, attach event listeners and actions
*/
initRTC = async () => {
const {appId} = this.state;
this._rtcEngine = await RtcEngine.create(appId);
await this._rtcEngine.enableVideo();
this._rtcEngine.addListener('Error', (err) => {
console.log('Error', err);
});
this._rtcEngine.addListener('UserJoined', (uid) => {
// Get current peer IDs
const {peerIds, inCall, seniors, channelName} = this.state;
// If new user
if (peerIds.indexOf(uid) === -1) {
if (inCall && seniors.length < 2) {
this._rtmEngine?.sendMessageByChannelId(
'lobby',
channelName + ':' + (peerIds.length + 2),
);
}
this.setState({
// Add peer ID to state array
peerIds: [...peerIds, uid],
});
}
});
this._rtcEngine.addListener('UserOffline', (uid) => {
const {peerIds} = this.state;
this.setState({
// Remove peer ID from state array
peerIds: peerIds.filter((id) => id !== uid),
});
});
// If Local user joins RTC channel
this._rtcEngine.addListener(
'JoinChannelSuccess',
(channel, uid, elapsed) => {
console.log('JoinChannelSuccess', channel, uid, elapsed);
this.setState({
inCall: true,
});
},
);
};
...
我们使用 App ID 创建我们的引擎实例,用 enableVideo
方法设置视频模式下的 SDK。
当我们加入频道时,RTC 为每个在线用户和后加入的新用户触发 userJoined
事件。当用户离开频道时,触发 userOffline
事件。我们使用事件监听器来更新 peerIds 数组中的 UID。稍后我们会使用这个数组来渲染其他用户的视频源。
一旦我们加入一个频道,SDK 就会触发 JoinChannelSuccess
事件。我们将状态变量 inCall
设置为true,以渲染视频聊天的用户界面。
当有新用户加入我们的视频聊天室时,如果我们是上文提到的高级成员,我们会用 lobby
RTM 频道向各频道的所有成员发送频道消息,更新的用户数量。
RTM 初始化
...
/**
* @name initRTM
* @description Function to initialize the Rtm Engine, attach event listeners and use them to sync usernames
*/
initRTM = async () => {
let {appId, myUsername} = this.state;
this._rtmEngine = new RtmEngine();
this._rtmEngine.on('error', (evt) => {
console.log(evt);
});
this._rtmEngine.on('channelMemberJoined', (evt) => {
let {channelName, seniors, peerIds, inCall} = this.state;
let {channelId, uid} = evt;
// if we're in call and receive a lobby message and also we're the senior member (oldest member in the channel), signal channel status to joined peer
if (inCall && channelId === 'lobby' && seniors.length < 2) {
this._rtmEngine
?.sendMessageToPeer({
peerId: uid,
text: channelName + ':' + (peerIds.length + 1),
offline: false,
})
.catch((e) => console.log(e));
}
});
this._rtmEngine.on('channelMemberLeft', (evt) => {
let {channelId, uid} = evt;
let {channelName, seniors, inCall, peerIds, rooms} = this.state;
if (channelName === channelId) {
// Remove seniors UID from state array
this.setState({
seniors: seniors.filter((id) => id !== uid),
rooms: {...rooms, [channelName]: peerIds.length},
});
if (inCall && seniors.length < 2) {
// if we're in call and we're the senior member (oldest member in the channel), signal channel status to all users
this._rtmEngine
?.sendMessageByChannelId(
'lobby',
channelName + ':' + (peerIds.length + 1),
)
.catch((e) => console.log(e));
}
}
});
this._rtmEngine.on('channelMessageReceived', (evt) => {
// received message is of the form - channel:membercount, add it to the state
let {text} = evt;
let data = text.split(':');
this.setState({rooms: {...this.state.rooms, [data[0]]: data[1]}});
});
this._rtmEngine.on('messageReceived', (evt) => {
// received message is of the form - channel:membercount, add it to the state
let {text} = evt;
let data = text.split(':');
this.setState({rooms: {...this.state.rooms, [data[0]]: data[1]}});
});
await this._rtmEngine.createClient(appId).catch((e) => console.log(e));
await this._rtmEngine
?.login({uid: myUsername})
.catch((e) => console.log(e));
await this._rtmEngine?.joinChannel('lobby').catch((e) => console.log(e));
this.setState({inLobby: true});
};
...
我们用 RTM 发送聊天室名称和成员数量。我们有一个高级成员(在我们之前加入聊天室的成员)的数组。如果高级成员数< 2,说明我们是最早加入的成员,负责发送信号(本地用户也是数组的一部分)。
首先,添加 channelMemberJoined
和 channelMemberLeft
监听器,当用户加入或离开 RTM 频道时,会分别触发这两个监听器。如果我们是最早进入聊天室的成员,当有用户加入大厅频道时,我们会向他们发送一条点对点消息。如果有成员离开当前视频聊天频道,我们会更新高级成员数组(如果该用户先于我们加入聊天室,我们会从数组中将其删除)。如果我们是更新成员数量的高级成员,还会向大厅发送一个频道消息。
接下来,添加 channelMessageReceived
和 messageReceived
事件监听器, 这两个监听器分别在我们收到频道消息和点对点消息时触发。我们拆分 channelName:memberCount
字符串(例如, ‘helloWorld:5’
), 用这两个数据来更新我们的字典。(例如,rooms: { ‘helloWorld’: 5, ‘roomTwo’: 3 }
)。
加入一个通话
...
/**
* @name joinCall
* @description Function to join a room and start the call
*/
joinCall = async (channelName: string) => {
this.setState({channelName});
let {token} = this.state;
// Join RTC Channel using null token and channel name
await this._rtcEngine?.joinChannel(token, channelName, null, 0);
await this._rtmEngine
?.joinChannel(channelName)
.catch((e) => console.log(e));
let {members} = await this._rtmEngine?.getChannelMembersBychannelId(
channelName,
);
// if we're the only member, broadcast to room to all users on RTM
if (members.length === 1) {
await this._rtmEngine
?.sendMessageByChannelId('lobby', channelName + ':' + 1)
.catch((e) => console.log(e));
}
this.setState({
inLobby: false,
seniors: members.map((m: any) => m.uid),
});
};
...
我们定义了一个加入通话的函数,该函数把频道名称作为参数。我们用频道名更新状态,并在 RTM 和 RTC 上使用 joinChannel
方法加入频道。
我们使用 RTM 上的 getChannelMembersBychannelId
方法获取频道上用户的 UID。如果我们是唯一的成员,我们就在 RTM 上向大厅频道发送频道消息,为所有用户更新创建房间的信息。
离开通话
...
/**
* @name endCall
* @description Function to end the call and return to lobby
*/
endCall = async () => {
let {channelName, myUsername, peerIds, seniors} = this.state;
// if we're the senior member, broadcast room to all users before leaving
if (seniors.length < 2) {
await this._rtmEngine
?.sendMessageByChannelId('lobby', channelName + ':' + peerIds.length)
.catch((e) => console.log(e));
}
await this._rtcEngine?.leaveChannel();
await this._rtmEngine?.logout();
await this._rtmEngine?.login({uid: myUsername});
await this._rtmEngine?.joinChannel('lobby');
this.setState({
peerIds: [],
inCall: false,
inLobby: true,
seniors: [],
channelName: '',
});
};
...
我们离开 RTM 和 RTC 视频聊天室频道,但仍保持与 RTM 的大厅频道的链接,继续接收更新。我们通过清除 peerIds
数组、 seniors
数组和 channelName
来更新我们的状态。我们还将 inCall
设置为false, inLobby
设置为 true 来渲染大厅的用户界面。
渲染用户界面
...
render() {
const {inCall, channelName, inLobby} = this.state;
return (
<SafeAreaView style={styles.max}>
<View style={styles.spacer}>
<Text style={styles.roleText}>
{inCall ? "You're in " + channelName : 'Lobby: Join/Create a room'}
</Text>
</View>
{this._renderRooms()}
{this._renderCall()}
{!inLobby && !inCall ? (
<Text style={styles.waitText}>Please wait, joining room...</Text>
) : null}
</SafeAreaView>
);
}
...
我们定义了显示按钮的渲染函数,如果我们正在通话中或者在大厅里,就可以显示状态。
...
_renderRooms = () => {
const {inLobby, rooms, input} = this.state;
return inLobby ? (
<View style={styles.fullView}>
<Text style={styles.subHeading}>Room List</Text>
<ScrollView>
{Object.keys(rooms).map((key, index) => {
return (
<TouchableOpacity
key={index}
onPress={() => this.joinCall(key)}
style={styles.roomsBtn}>
<Text>
<Text style={styles.roomHead}>{key}</Text>
<Text style={styles.whiteText}>
{' (' + rooms[key] + ' users)'}
</Text>
</Text>
</TouchableOpacity>
);
})}
<Text>
{Object.keys(rooms).length === 0
? 'No active rooms, please create new room'
: null}
</Text>
</ScrollView>
<TextInput
value={input}
onChangeText={(val) => this.setState({input: val})}
style={styles.input}
placeholder="Enter Room Name"
/>
<TouchableOpacity
onPress={async () => {
input ? await this.joinCall(input) : null;
}}
style={styles.button}>
<Text style={styles.buttonText}>Create Room</Text>
</TouchableOpacity>
</View>
) : null;
};
...
我们使用 _renderRooms
函数渲染一个滚动视图, 该视图在房间字典上迭代,显示已创建的房间列表及其成员数。用户可以点击加入任何房间,这就调用了 joinCall
函数。我们还渲染一个文本输入,用户使用该输入调用相同的 joinCall
函数来创建聊天室。
...
_renderCall = () => {
const {inCall, peerIds, channelName} = this.state;
return inCall ? (
<View style={styles.fullView}>
<RtcLocalView.SurfaceView
style={styles.video}
channelId={channelName}
renderMode={VideoRenderMode.Hidden}
/>
<ScrollView>
{peerIds.map((key, index) => {
return (
<RtcRemoteView.SurfaceView
channelId={channelName}
renderMode={VideoRenderMode.Hidden}
key={index}
uid={key}
style={styles.video}
/>
);
})}
</ScrollView>
<TouchableOpacity onPress={this.endCall} style={styles.button}>
<Text style={styles.buttonText}>Leave Room</Text>
</TouchableOpacity>
</View>
) : null;
};
}
当我们连接到视频聊天室后,我们使用 _renderCall
函数来渲染视频。我们使用 SDK 中的 RtcLocalView
组件来渲染我们自己(本地用户)的视频。我们在滚动视图中使用 RtcRemoteView
渲染已连接用户的视频,这些已连接用户以 UID 存储在 peerIds
数组中。我们还会显示一个用来离开聊天室的按钮。
Permission.ts
import {PermissionsAndroid} from 'react-native'
/**
* @name requestCameraAndAudioPermission
* @description Function to request permission for Audio and Camera
*/
export default async function requestCameraAndAudioPermission() {
try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
])
if (
granted['android.permission.RECORD_AUDIO'] === PermissionsAndroid.RESULTS.GRANTED
&& granted['android.permission.CAMERA'] === PermissionsAndroid.RESULTS.GRANTED
) {
console.log('You can use the cameras & mic')
} else {
console.log('Permission denied')
}
} catch (err) {
console.warn(err)
}
}
我们正在导出一个辅助函数,这个辅助函数可以向安卓操作系统请求麦克风权限。
Style.ts
Style.ts 文件包含了组件的样式。
import {StyleSheet} from 'react-native';
export default StyleSheet.create({
max: {
flex: 1,
backgroundColor: '#F7F7F7',
},
button: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#38373A',
marginBottom: 16,
},
buttonText: {
color: '#fff',
textAlign: 'center',
},
fullView: {
flex: 5,
alignContent: 'center',
marginHorizontal: 24,
},
subHeading: {
fontSize: 16,
fontWeight: '700',
marginBottom: 10,
},
waitText: {
marginTop: 50,
fontSize: 16,
fontWeight: '700',
textAlign: 'center',
},
roleText: {
textAlign: 'center',
// fontWeight: '700',
color: '#fbfbfb',
fontSize: 18,
},
spacer: {
width: '100%',
padding: '2%',
marginBottom: 32,
// borderWidth: 1,
backgroundColor: '#38373A',
color: '#fbfbfb',
// borderColor: '#38373A',
},
input: {
height: 40,
borderColor: '#38373A',
borderWidth: 1.5,
width: '100%',
alignSelf: 'center',
padding: 10,
marginBottom: 10,
},
roomsBtn: {
padding: 8,
marginBottom: 4,
backgroundColor: '#38373A',
},
roomHead: {fontWeight: 'bold', color: '#fff', fontSize: 16},
whiteText: {color: '#fff'},
video: {width: 150, height: 150},
});
其他
我们可以用同样的技术来传达其他信息,比如连接的用户名称、聊天室介绍和聊天室标题。我们甚至可以用同样的机制,通过发送 RTM 消息把用户踢出通话,只需要调用远端用户设备上的离开频道方法。
总结
相信大家已经学会了如何用声网Agora RTM SDK 分享信息以及动态创建视频聊天室。
如果你还想了解能给实时参与应用添加更多功能的方法,可以查看 Agora React Native API 哦~~~
获取更多文档、Demo、技术帮助
- 获取 SDK 开发文档,可访问声网文档中心。
- 如需参考各类场景 Demo,可访问下载中心。
- 如遇开发疑难,可访问论坛发帖提问。
- 了解更多教程、RTE 技术干货与技术活动,可访问声网开发者社区。
- 欢迎扫码关注我们。
原文作者:Ekaansh Arora
原文链接:Dynamic Channels for Video Chat Using Agora RTM on React Native
channelMessageReceived这个事件自己发送到频道消息之后会触发吗