大数跨境
0
0

Agora 教程|用Agora开发实时音视频直播React Native App

Agora 教程|用Agora开发实时音视频直播React Native App RTE开发者社区
2021-06-10
0
导读:从主播、采访到现场音乐表演,实时音视频直播在广泛的用途上越来越受欢迎。一旦有一些用户与观众进行实时互动,你就

从主播、采访到现场音乐表演,实时音视频直播在广泛的用途上越来越受欢迎。一旦有一些用户与观众进行实时互动,你就会发现无限的可能性。

有一个简单的方法可以利用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,
9from '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 ID

  • token:为加入频道而产生的Token。

  • isHost: 在观众和直播之间切换的布尔值。

  • channelName:频道名称

  • joinSucceed存储我们是否成功连接的布尔值。

  • rtcUid: 本地用户加入RTC频道时的UID。

  • myUsername:登录RTM的本地用户名称。

  • usernames:将远程用户的RTC UID与他们的用户名关联起来的字典,我们将使用RTM获取该用户名。

  • peerIds:一个数组,用于存储通道中其他用户的UID。

 1         ...
2        export default class App extends Component<nullState{
3          _rtcEngine?: RtcEngine;
4          _rtmEngine?: RtmEngine;
5
6      constructor(props) {
7        super(props);
8        this.state = {
9          appId'YOUR APP ID HERE',
10          tokennull,
11          isHosttrue,
12          channelName'channel-x',
13          joinSucceedfalse,
14          rtcUidparseInt((new Date().getTime() + '').slice(413), 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: [], joinSucceedfalseusernames: {} });
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: 5color: '#38373A' },
92});

Style.ts 文件包含了组件的样式。

结论

这就是构建一个实时音视频直播App的简单方法。你可以点击【阅读原文】参考声网React Native API参考中的方法,这些方法可以帮助你快速添加功能,如静音麦克风、设置音频配置文件、音频混合等。


获取更多文档、Demo、技术帮助

  • 获取 SDK 开发文档,可访问声网文档中心。

  • 如需参考各类场景 Demo,可访问声网下载中心获取。

  • 如遇开发疑难,可访问论坛发帖提问。

  • 了解更多教程、RTE 技术干货与技术活动,可访问声网开发者社区。

  • 欢迎扫码关注我们。

上述文档都可点【阅读原文】进行查看~

【声明】内容源于网络
0
0
RTE开发者社区
RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
内容 1122
粉丝 0
RTE开发者社区 RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
总阅读653
粉丝0
内容1.1k