
从主播、采访到现场音乐表演,实时音视频直播在广泛的用途上越来越受欢迎。一旦有一些用户与观众进行实时互动,你就会发现无限的可能性。
有一个简单的方法可以利用Agora React Native SDK来完成音频直播。在本教程中,我们将通过利用Agora音频SDK来构建一个可以拥有多个主播并承载成千上万用户的音频直播App。在深入研究代码之前,我们将介绍应用程序的结构、设置和执行。
GitHub开源示例代码:https://github.com/EkaanshArora/Agora-RN-Audio-Broadcast
我们将使用 Agora RTC SDK for React Native 来完成示例。在写这篇文章的时候,我使用的是v3.2.2。
创建一个Agora账户
首先,需要注册声网开发者账户(https://sso.agora.io/cn/v4/signup) 并登录到后台。

导航到 "项卡下的 "项目列表 "选项卡,并通过单击蓝色的 "创建 "按钮创建一个项目。
创建项目并获得App ID。(当提示使用App ID+证书时,请选择App ID Only。)App ID将在您开发应用程序时用于授权请求,而无需生成Token。
注意:本指南没有实现Token鉴权,建议所有在生产环境中运行的RTE应用都采用Token鉴权。有关Agora平台内基于Token鉴权的更多信息,请参考校验用户权限(https://docs.agora.io/cn/Video/token?platform=All%20Platforms)。
例子的结构
这就是应用程序的结构。
1.
2├── android
3├── components
4│ └── Permission.ts
5│ └── Style.ts
6├── ios
7├── App.tsx
8├── index.js
9.
运行应用程序
你需要安装最新版本的Node.js和NPM。
确保你已经拥有Agora账户,设置了一个项目,并生成了一个App ID(如上所述)。
从 GitHub示例项目主分支下载并解压ZIP文件。
运行
npm install来安装解压目录中的App依赖项。导航到
./App.tsx,在状态声明中输入 App ID 作为appId: YourAppIdHere.如果你是为iOS构建,打开终端,执行
cd ios && pod install。连接设备,并运行
npx react-native run-android/npx react-native run-ios来启动应用程序。给它几分钟的时间来构建应用程序并安装到你的设备上。一旦你在移动设备(或模拟器)上看到主屏幕,点击设备上的开始通话按钮。
就是这样,现在你应该已经在两个设备之间建立了音频直播。
该应用使用 channel-x 作为频道名称。
在我们深入研究代码之前,让我们先把一些基础知识讲清楚
我们使用Agora RTC (实时音视频) SDK来连接到一个频道并加入一个音频通话.
可以有多个用户对一个频道进行直播。所有的用户作为该频道的听众,都可以收听主播的声音。
听众可以动态切换到主播角色。
Agora RTC SDK为每个用户使用唯一的ID(UID)。为了将这些UID与用户名关联起来,我们将使用Agora RTM(实时消息)SDK向通话中的其他人发送用户名。我们将在下面讨论它是如何完成的。
让我们来看看代码是如何工作的:
App.tsx
App.tsx 将是进入应用程序的入口。我们的所有代码都会在这个文件中。当你打开应用程序时,会有一个用户名字段,里面有三个按钮:加入通话,结束通话,以及在主播和观众之间切换我们的用户角色。
1import React, { Component } from 'react';
2import {
3 Platform,
4 ScrollView,
5 Text,
6 TextInput,
7 TouchableOpacity,
8 View,
9} from 'react-native';
10import RtcEngine, { ClientRole, ChannelProfile } from 'react-native-agora';
11import requestCameraAndAudioPermission from './components/Permission';
12import styles from './components/Style';
13import RtmEngine from 'agora-react-native-rtm';
14
15interface Props {}
16
17/**
18 * @property appId Agora App ID
19 * @property token Token for the channel;
20 * @property isHost Boolean value to select between broadcaster and audience
21 * @property channelName Channel Name for the current session
22 * @property joinSucceed State variable for storing success
23 * @property rtcUid local user's UID on joining the RTC channel
24 * @property peerIds Array for storing connected peers
25 * @property myUsername local user's name to login to RTM
26 * @property Array to store usernames mapped to RTC UIDs
27 */
28
29interface State {
30 appId: string;
31 token: string | null;
32 isHost: boolean;
33 channelName: string;
34 joinSucceed: boolean;
35 rtcUid: number;
36 peerIds: number[];
37 myUsername: string;
38 usernames: { [uid: string]: string };
39}
40...
我们首先编写使用过的import声明。接下来,为我们的应用状态定义一个接口,包含以下内容。
appId:Agora App IDtoken:为加入频道而产生的Token。isHost: 在观众和直播之间切换的布尔值。channelName:频道名称joinSucceed存储我们是否成功连接的布尔值。rtcUid: 本地用户加入RTC频道时的UID。myUsername:登录RTM的本地用户名称。usernames:将远程用户的RTC UID与他们的用户名关联起来的字典,我们将使用RTM获取该用户名。peerIds:一个数组,用于存储通道中其他用户的UID。
1 ...
2 export default class App extends Component<null, State> {
3 _rtcEngine?: RtcEngine;
4 _rtmEngine?: RtmEngine;
5
6 constructor(props) {
7 super(props);
8 this.state = {
9 appId: 'YOUR APP ID HERE',
10 token: null,
11 isHost: true,
12 channelName: 'channel-x',
13 joinSucceed: false,
14 rtcUid: parseInt((new Date().getTime() + '').slice(4, 13), 10),
15 peerIds: [],
16 myUsername: '',
17 usernames: {},
18 };
19 if (Platform.OS === 'android') {
20 // Request required permissions from Android
21 requestCameraAndAudioPermission().then(() => {
22 console.log('requested!');
23 });
24 }
25 }
26
27 componentDidMount() {
28 this.initRTC();
29 this.initRTM();
30 }
31
32 componentWillUnmount() {
33 this._rtmEngine?.destroyClient();
34 this._rtcEngine?.destroy();
35 }
36 ...
我们定义一个基于类的组件:_rtcEngine 变量将存储 RtcEngine 类的实例,_rtmEngine 变量将存储 RtmEngine 类的实例,我们可以用它来访问 SDK 函数。
在构造函数中,设置我们的状态变量,并申请在 Android 上录制音频的权限。(我们使用权限中的帮助函数,如下所述)。当组件被挂载时,我们调用 initRTC 和 initRTM 函数,它们使用 App ID 初始化 RTC 和 RTM 引擎。当组件卸载时,我们销毁我们的引擎实例。
RTC初始化
1...
2 /**
3 * @name initRTC
4 * @description Function to initialize the Rtc Engine, attach event listeners and actions
5 */
6 initRTC = async () => {
7 const { appId, isHost } = this.state;
8 this._rtcEngine = await RtcEngine.create(appId);
9 // await this._rtcEngine.disableVideo();
10 await this._rtcEngine.setChannelProfile(ChannelProfile.LiveBroadcasting);
11 await this._rtcEngine.setClientRole(
12 isHost ? ClientRole.Broadcaster : ClientRole.Audience
13 );
14
15 this._rtcEngine.addListener('Error', (err) => {
16 console.log('Error', err);
17 });
18
19 this._rtcEngine.addListener('UserJoined', (uid, elapsed) => {
20 console.log('UserJoined', uid, elapsed);
21 // Get current peer IDs
22 const { peerIds } = this.state;
23 // If new user
24 if (peerIds.indexOf(uid) === -1) {
25 this.setState({
26 // Add peer ID to state array
27 peerIds: [...peerIds, uid],
28 });
29 }
30 });
31
32 this._rtcEngine.addListener('UserOffline', (uid, reason) => {
33 console.log('UserOffline', uid, reason);
34 const { peerIds } = this.state;
35 this.setState({
36 // Remove peer ID from state array
37 peerIds: peerIds.filter((id) => id !== uid),
38 });
39 });
40
41 // If Local user joins RTC channel
42 this._rtcEngine.addListener(
43 'JoinChannelSuccess',
44 (channel, uid, elapsed) => {
45 console.log('JoinChannelSuccess', channel, uid, elapsed);
46 this.setState({
47 joinSucceed: true,
48 rtcUid: uid,
49 });
50 }
51 );
52 };
53...
使用App ID来创建我们的引擎实例。接下来,根据我们的isHost状态变量值,将channelProfile设置为Live Broadcasting和clientRole。
当我们加入频道时,RTC为每个在场的用户和后来加入的新用户触发一个userJoined事件。当用户离开通道时,会触发userOffline事件。我们使用事件监听器来同步我们的peerIds数组。
注意:观众成员不会触发userJoined/userOffline事件。
RTM初始化
使用RTM将我们的用户名发送给通话中的其他用户。这就是如何将我们的用户名与我们的RTC UID关联起来的方法。
当一个用户加入一个频道时,以
UID:Username的形式向所有频道成员发送 一条消息。在收到一条频道消息时,所有用户都会将键值对添加到他们的用户名字典中。
当一个新用户加入时,频道上的所有成员都会以相同的模式
UID:Username向该用户发送一条对等消息 。在接收到对等消息时,我们也做同样的事情(将键值对添加到字典中)并更新我们的用户名。
1 ...
2 /**
3 * @name initRTM
4 * @description Function to initialize the Rtm Engine, attach event listeners and use them to sync usernames
5 */
6 initRTM = async () => {
7 let { appId, usernames, rtcUid } = this.state;
8 this._rtmEngine = new RtmEngine();
9
10 this._rtmEngine.on('error', (evt) => {
11 console.log(evt);
12 });
13
14 this._rtmEngine.on('channelMessageReceived', (evt) => {
15 let { text } = evt;
16 let data = text.split(':');
17 console.log('cmr', evt);
18 if (data[1] === '!leave') {
19 let temp = JSON.parse(JSON.stringify(usernames));
20 Object.keys(temp).map((k) => {
21 if (k === data[0]) delete temp[k];
22 });
23 this.setState({
24 usernames: temp,
25 });
26 } else {
27 this.setState({
28 usernames: { ...usernames, [data[0]]: data[1] },
29 });
30 }
31 });
32
33 this._rtmEngine.on('messageReceived', (evt) => {
34 let { text } = evt;
35 let data = text.split(':');
36 console.log('pm', evt);
37 this.setState({
38 usernames: { ...usernames, [data[0]]: data[1] },
39 });
40 });
41
42 this._rtmEngine.on('channelMemberJoined', (evt) => {
43 console.log('!spm', this.state.myUsername);
44 this._rtmEngine?.sendMessageToPeer({
45 peerId: evt.uid,
46 text: rtcUid + ':' + this.state.myUsername,
47 offline: false,
48 });
49 });
50
51 await this._rtmEngine.createClient(appId).catch((e) => console.log(e));
52 };
53 ...
按照计划,我们在 channelMessageReceived (向频道广播消息)、 messageReceived (对等消息)和 channelMemberJoined 事件上附加带有函数的事件监听器来填充和更新用户名。我们还使用相同的App ID在引擎上创建一个客户端。
按钮的功能
1...
2 /**
3 * @name toggleRole
4 * @description Function to toggle the roll between broadcaster and audience
5 */
6 toggleRole = async () => {
7 this._rtcEngine?.setClientRole(
8 !this.state.isHost ? ClientRole.Broadcaster : ClientRole.Audience
9 );
10 this.setState((ps) => {
11 return { isHost: !ps.isHost };
12 });
13 };
14
15 /**
16 * @name startCall
17 * @description Function to start the call
18 */
19 startCall = async () => {
20 let { myUsername, token, channelName, rtcUid } = this.state;
21 if (myUsername) {
22 // Join RTC Channel using null token and channel name
23 await this._rtcEngine?.joinChannel(token, channelName, null, rtcUid);
24 // Login & Join RTM Channel
25 await this._rtmEngine
26 ?.login({ uid: myUsername })
27 .catch((e) => console.log(e));
28 await this._rtmEngine
29 ?.joinChannel(channelName)
30 .catch((e) => console.log(e));
31 await this._rtmEngine
32 ?.sendMessageByChannelId(channelName, rtcUid + ':' + myUsername)
33 .catch((e) => console.log(e));
34 }
35 };
36
37 /**
38 * @name endCall
39 * @description Function to end the call
40 */
41 endCall = async () => {
42 let { channelName, rtcUid } = this.state;
43 await this._rtcEngine?.leaveChannel();
44 await this._rtmEngine
45 ?.sendMessageByChannelId(channelName, rtcUid + ':!leave')
46 .catch((e) => console.log(e));
47 this.setState({ peerIds: [], joinSucceed: false, usernames: {} });
48 await this._rtmEngine?.logout().catch((e) => console.log(e));
49 };
50...
toggleRole 函数更新状态并根据状态调用具有正确参数的 setClientRole 函数。
startCall 函数检查是否有用户名被输入,然后加入RTC通道。它也会登录到RTM,加入频道,并为用户名发送频道消息,就像我们之前讨论的那样。
endCall 函数离开RTC通道,发送一条消息从我们的远程用户字典中删除用户名,然后离开并退出RTM。
渲染用户界面
1...
2 render() {
3 const { joinSucceed, isHost, channelName, myUsername } = this.state;
4 return (
5 <View style={styles.max}>
6 <View style={styles.spacer}>
7 <Text style={styles.roleText}>
8 You're{' '}
9 <Text style={styles.roleTextBold}>
10 {isHost ? 'a broadcaster' : 'the audience'}
11 </Text>
12 </Text>
13 <Text style={styles.roleText}>
14 {joinSucceed
15 ? "You're connected to " + channelName
16 : "You're disconnected - start call"}
17 </Text>
18 </View>
19 {this._renderUsers()}
20 {joinSucceed ? (
21 <></>
22 ) : (
23 <>
24 <TextInput
25 style={styles.input}
26 placeholder={'Name'}
27 onChangeText={(t) => {
28 this.setState({ myUsername: t });
29 }}
30 value={myUsername}
31 />
32 {!myUsername ? (
33 <Text style={styles.errorText}>Name can't be blank</Text>
34 ) : null}
35 </>
36 )}
37 <View style={styles.buttonHolder}>
38 <TouchableOpacity onPress={this.toggleRole} style={styles.button}>
39 <Text style={styles.buttonText}> Toggle Role </Text>
40 </TouchableOpacity>
41 <TouchableOpacity onPress={this.startCall} style={styles.button}>
42 <Text style={styles.buttonText}> Start Call </Text>
43 </TouchableOpacity>
44 <TouchableOpacity onPress={this.endCall} style={styles.button}>
45 <Text style={styles.buttonText}> End Call </Text>
46 </TouchableOpacity>
47 </View>
48 </View>
49 );
50 }
51
52 _renderUsers = () => {
53 const { joinSucceed, peerIds, isHost, usernames, myUsername } = this.state;
54
55 return joinSucceed ? (
56 <View style={styles.fullView}>
57 <Text style={styles.subHeading}>Broadcaster List</Text>
58 {isHost ? <Text>{myUsername}</Text> : <></>}
59 <ScrollView>
60 {peerIds.map((value, index) => {
61 return <Text key={index}>{usernames[value + '']}</Text>;
62 })}
63 </ScrollView>
64 <Text style={styles.subHeading}>Audience List</Text>
65 {!isHost ? <Text>{myUsername}</Text> : <></>}
66 <ScrollView>
67 {Object.keys(usernames).map((key, index) => {
68 return (
69 <Text key={index}>
70 {peerIds.includes(parseInt(key, 10)) ? null : usernames[key]}
71 </Text>
72 );
73 })}
74 </ScrollView>
75 </View>
76 ) : null;
77 };
78}
79...
80
我们定义了渲染函数,用于显示开始和结束调用的按钮以及切换角色。我们定义了一个函数 _renderUsers 用于渲染所有直播和观众成员的列表。
权限
1import {PermissionsAndroid} from 'react-native'
2
3/**
4 * @name requestCameraAndAudioPermission
5 * @description Function to request permission for Audio and Camera
6 */
7export default async function requestCameraAndAudioPermission() {
8 try {
9 const granted = await PermissionsAndroid.requestMultiple([
10 PermissionsAndroid.PERMISSIONS.CAMERA,
11 PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
12 ])
13 if (
14 granted['android.permission.RECORD_AUDIO'] === PermissionsAndroid.RESULTS.GRANTED
15 && granted['android.permission.CAMERA'] === PermissionsAndroid.RESULTS.GRANTED
16 ) {
17 console.log('You can use the cameras & mic')
18 } else {
19 console.log('Permission denied')
20 }
21 } catch (err) {
22 console.warn(err)
23 }
24}
我们正在导出一个辅助函数来向Android操作系统申请麦克风权限。
样式
1import { StyleSheet } from 'react-native';
2
3export default StyleSheet.create({
4 max: {
5 flex: 1,
6 // marginVertical: 40,
7 backgroundColor: '#F7F7F7',
8 },
9 buttonHolder: {
10 alignItems: 'center',
11 flex: 1,
12 flexDirection: 'row',
13 justifyContent: 'space-evenly',
14 },
15 button: {
16 paddingHorizontal: 16,
17 paddingVertical: 8,
18 backgroundColor: '#38373A',
19 // borderRadius: 24,
20 },
21 buttonRed: {
22 paddingHorizontal: 16,
23 paddingVertical: 8,
24 // backgroundColor: '#F4061D',
25 borderRadius: 24,
26 },
27 buttonGreen: {
28 paddingHorizontal: 16,
29 paddingVertical: 8,
30 // backgroundColor: '#09DF18',
31 borderRadius: 24,
32 },
33 buttonText: {
34 color: '#fff',
35 },
36 fullView: {
37 flex: 5,
38 alignContent: 'center',
39 marginHorizontal: 24,
40 },
41 centerText: {
42 textAlign: 'center',
43 },
44 subHeading: {
45 fontSize: 16,
46 fontWeight: '700',
47 },
48 remote: {
49 width: 150,
50 height: 150,
51 marginHorizontal: 2.5,
52 },
53 noUserText: {
54 paddingHorizontal: 10,
55 paddingVertical: 5,
56 color: '#0093E9',
57 },
58 roleText: {
59 textAlign: 'center',
60 // fontWeight: '700',
61 color: '#fbfbfb',
62 fontSize: 18,
63 },
64 roleTextBold: {
65 textAlign: 'center',
66 fontSize: 18,
67 fontWeight: '700',
68 },
69 roleTextRed: {
70 textAlign: 'center',
71 fontSize: 18,
72 // color: '#F4061D',
73 },
74 spacer: {
75 width: '100%',
76 padding: '2%',
77 marginBottom: 32,
78 // borderWidth: 1,
79 backgroundColor: '#38373A',
80 color:'#fbfbfb',
81 // borderColor: '#38373A',
82 },
83 input: {
84 height: 40,
85 borderColor: '#38373A',
86 borderWidth: 1.5,
87 width: '90%',
88 alignSelf: 'center',
89 padding: 10,
90 },
91 errorText: { textAlign: 'center', margin: 5, color: '#38373A' },
92});
Style.ts 文件包含了组件的样式。
结论
这就是构建一个实时音视频直播App的简单方法。你可以点击【阅读原文】参考声网React Native API参考中的方法,这些方法可以帮助你快速添加功能,如静音麦克风、设置音频配置文件、音频混合等。
获取更多文档、Demo、技术帮助
获取 SDK 开发文档,可访问声网文档中心。
如需参考各类场景 Demo,可访问声网下载中心获取。
如遇开发疑难,可访问论坛发帖提问。
了解更多教程、RTE 技术干货与技术活动,可访问声网开发者社区。
欢迎扫码关注我们。
上述文档都可点击【阅读原文】进行查看~


