从主播、采访到现场音乐表演,实时音视频直播在广泛的用途上越来越受欢迎。一旦有一些用户与观众进行实时互动,你就会发现无限的可能性。
有一个简单的方法可以利用Agora React Native SDK来完成音频直播。在本教程中,我们将通过利用Agora音频SDK来构建一个可以拥有多个主播并承载成千上万用户的音频直播App。在深入研究代码之前,我们将介绍应用程序的结构、设置和执行。点击这里可以查看GitHub开源示例代码。
我们将使用 Agora RTC SDK for React Native 来完成示例。在写这篇文章的时候,我使用的是v3.2.2。
创建一个Agora账户
Selecting the Project Management tab in the Agora Console.
导航到 "项目管理 "选项卡下的 "项目列表 "选项卡,并通过单击蓝色的 "创建 "按钮创建一个项目。
创建项目并获得App ID。(当提示使用App ID+证书时,请选择App ID Only。)App ID将在您开发应用程序时用于授权请求,而无需生成Token。
注意:本指南没有实现Token鉴权,建议所有在生产环境中运行的RTE应用都采用Token鉴权。有关Agora平台内基于Token鉴权的更多信息,请参考校验用户权限。
例子的结构
这就是应用程序的结构。
.
├── android
├── components
│ └── Permission.ts
│ └── Style.ts
├── ios
├── App.tsx
├── index.js
.
运行应用程序
你需要安装最新版本的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将是进入应用程序的入口。我们的所有代码都会在这个文件中。当你打开应用程序时,会有一个用户名字段,里面有三个按钮:加入通话,结束通话,以及在主播和观众之间切换我们的用户角色。
import React, { Component } from 'react';
import {
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import RtcEngine, { ClientRole, ChannelProfile } 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
* @property token Token for the channel;
* @property isHost Boolean value to select between broadcaster and audience
* @property channelName Channel Name for the current session
* @property joinSucceed State variable for storing success
* @property rtcUid local user's UID on joining the RTC channel
* @property peerIds Array for storing connected peers
* @property myUsername local user's name to login to RTM
* @property Array to store usernames mapped to RTC UIDs
*/
interface State {
appId: string;
token: string | null;
isHost: boolean;
channelName: string;
joinSucceed: boolean;
rtcUid: number;
peerIds: number[];
myUsername: string;
usernames: { [uid: string]: string };
}
...
我们首先编写使用过的import声明。接下来,为我们的应用状态定义一个接口,包含以下内容。
-
appId
:Agora App ID -
token
:为加入频道而产生的Token。 -
isHost
: 在观众和直播之间切换的布尔值。 -
channelName
:频道名称 -
joinSucceed
存储我们是否成功连接的布尔值。 -
rtcUid
: 本地用户加入RTC频道时的UID。 -
myUsername
:登录RTM的本地用户名称。 -
usernames
:将远程用户的RTC UID与他们的用户名关联起来的字典,我们将使用RTM获取该用户名。 -
peerIds
:一个数组,用于存储通道中其他用户的UID。... export default class App extends Component<null, State> { _rtcEngine?: RtcEngine; _rtmEngine?: RtmEngine; constructor(props) { super(props); this.state = { appId: 'YOUR APP ID HERE', token: null, isHost: true, channelName: 'channel-x', joinSucceed: false, rtcUid: parseInt((new Date().getTime() + '').slice(4, 13), 10), peerIds: [], myUsername: '', usernames: {}, }; if (Platform.OS === 'android') { // Request required permissions from Android requestCameraAndAudioPermission().then(() => { console.log('requested!'); }); } } componentDidMount() { this.initRTC(); this.initRTM(); } componentWillUnmount() { this._rtmEngine?.destroyClient(); this._rtcEngine?.destroy(); } ...
我们定义一个基于类的组件:_rtcEngine变量将存储RtcEngine类的实例,_rtmEngine变量将存储RtmEngine类的实例,我们可以用它来访问SDK函数。
在构造函数中,设置我们的状态变量,并申请在Android上录制音频的权限。(我们使用权限中的帮助函数,如下所述)。当组件被挂载时,我们调用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, isHost } = this.state;
this._rtcEngine = await RtcEngine.create(appId);
// await this._rtcEngine.disableVideo();
await this._rtcEngine.setChannelProfile(ChannelProfile.LiveBroadcasting);
await this._rtcEngine.setClientRole(
isHost ? ClientRole.Broadcaster : ClientRole.Audience
);
this._rtcEngine.addListener('Error', (err) => {
console.log('Error', err);
});
this._rtcEngine.addListener('UserJoined', (uid, elapsed) => {
console.log('UserJoined', uid, elapsed);
// Get current peer IDs
const { peerIds } = this.state;
// If new user
if (peerIds.indexOf(uid) === -1) {
this.setState({
// Add peer ID to state array
peerIds: [...peerIds, uid],
});
}
});
this._rtcEngine.addListener('UserOffline', (uid, reason) => {
console.log('UserOffline', uid, reason);
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({
joinSucceed: true,
rtcUid: uid,
});
}
);
};
...
使用App ID来创建我们的引擎实例。接下来,根据我们的isHost状态变量值,将channelProfile设置为Live Broadcasting和clientRole。
当我们加入频道时,RTC为每个在场的用户和后来加入的新用户触发一个userJoined事件。当用户离开通道时,会触发userOffline事件。我们使用事件监听器来同步我们的peerIds数组。
注意:观众成员不会触发userJoined/userOffline事件。
RTM初始化
使用RTM将我们的用户名发送给通话中的其他用户。这就是如何将我们的用户名与我们的RTC UID关联起来的方法。
-
当一个用户加入一个频道时,以
UID:Username
的形式向所有频道成员发送 一条消息。 -
在收到一条频道消息时,所有用户都会将键值对添加到他们的用户名字典中。
-
当一个新用户加入时,频道上的所有成员都会以相同的模式
UID:Username
向该用户发送一条对等消息 。 -
在接收到对等消息时,我们也做同样的事情(将键值对添加到字典中)并更新我们的用户名。
... /** * @name initRTM * @description Function to initialize the Rtm Engine, attach event listeners and use them to sync usernames */ initRTM = async () => { let { appId, usernames, rtcUid } = this.state; this._rtmEngine = new RtmEngine(); this._rtmEngine.on('error', (evt) => { console.log(evt); }); this._rtmEngine.on('channelMessageReceived', (evt) => { let { text } = evt; let data = text.split(':'); console.log('cmr', evt); if (data[1] === '!leave') { let temp = JSON.parse(JSON.stringify(usernames)); Object.keys(temp).map((k) => { if (k === data[0]) delete temp[k]; }); this.setState({ usernames: temp, }); } else { this.setState({ usernames: { ...usernames, [data[0]]: data[1] }, }); } }); this._rtmEngine.on('messageReceived', (evt) => { let { text } = evt; let data = text.split(':'); console.log('pm', evt); this.setState({ usernames: { ...usernames, [data[0]]: data[1] }, }); }); this._rtmEngine.on('channelMemberJoined', (evt) => { console.log('!spm', this.state.myUsername); this._rtmEngine?.sendMessageToPeer({ peerId: evt.uid, text: rtcUid + ':' + this.state.myUsername, offline: false, }); }); await this._rtmEngine.createClient(appId).catch((e) => console.log(e)); }; ...
按照计划,我们在 channelMessageReceived
(向频道广播消息)、 messageReceived
(对等消息)和 channelMemberJoined
事件上附加带有函数的事件监听器来填充和更新用户名。我们还使用相同的App ID在引擎上创建一个客户端。
按钮的功能
...
/**
* @name toggleRole
* @description Function to toggle the roll between broadcaster and audience
*/
toggleRole = async () => {
this._rtcEngine?.setClientRole(
!this.state.isHost ? ClientRole.Broadcaster : ClientRole.Audience
);
this.setState((ps) => {
return { isHost: !ps.isHost };
});
};
/**
* @name startCall
* @description Function to start the call
*/
startCall = async () => {
let { myUsername, token, channelName, rtcUid } = this.state;
if (myUsername) {
// Join RTC Channel using null token and channel name
await this._rtcEngine?.joinChannel(token, channelName, null, rtcUid);
// Login & Join RTM Channel
await this._rtmEngine
?.login({ uid: myUsername })
.catch((e) => console.log(e));
await this._rtmEngine
?.joinChannel(channelName)
.catch((e) => console.log(e));
await this._rtmEngine
?.sendMessageByChannelId(channelName, rtcUid + ':' + myUsername)
.catch((e) => console.log(e));
}
};
/**
* @name endCall
* @description Function to end the call
*/
endCall = async () => {
let { channelName, rtcUid } = this.state;
await this._rtcEngine?.leaveChannel();
await this._rtmEngine
?.sendMessageByChannelId(channelName, rtcUid + ':!leave')
.catch((e) => console.log(e));
this.setState({ peerIds: [], joinSucceed: false, usernames: {} });
await this._rtmEngine?.logout().catch((e) => console.log(e));
};
...
toggleRole
函数更新状态并根据状态调用具有正确参数的 setClientRole
函数。
startCall
函数检查是否有用户名被输入,然后加入RTC通道。它也会登录到RTM,加入频道,并为用户名发送频道消息,就像我们之前讨论的那样。
endCall
函数离开RTC通道,发送一条消息从我们的远程用户字典中删除用户名,然后离开并退出RTM。
渲染用户界面
...
render() {
const { joinSucceed, isHost, channelName, myUsername } = this.state;
return (
<View style={styles.max}>
<View style={styles.spacer}>
<Text style={styles.roleText}>
You're{' '}
<Text style={styles.roleTextBold}>
{isHost ? 'a broadcaster' : 'the audience'}
</Text>
</Text>
<Text style={styles.roleText}>
{joinSucceed
? "You're connected to " + channelName
: "You're disconnected - start call"}
</Text>
</View>
{this._renderUsers()}
{joinSucceed ? (
<></>
) : (
<>
<TextInput
style={styles.input}
placeholder={'Name'}
onChangeText={(t) => {
this.setState({ myUsername: t });
}}
value={myUsername}
/>
{!myUsername ? (
<Text style={styles.errorText}>Name can't be blank</Text>
) : null}
</>
)}
<View style={styles.buttonHolder}>
<TouchableOpacity onPress={this.toggleRole} style={styles.button}>
<Text style={styles.buttonText}> Toggle Role </Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.startCall} style={styles.button}>
<Text style={styles.buttonText}> Start Call </Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.endCall} style={styles.button}>
<Text style={styles.buttonText}> End Call </Text>
</TouchableOpacity>
</View>
</View>
);
}
_renderUsers = () => {
const { joinSucceed, peerIds, isHost, usernames, myUsername } = this.state;
return joinSucceed ? (
<View style={styles.fullView}>
<Text style={styles.subHeading}>Broadcaster List</Text>
{isHost ? <Text>{myUsername}</Text> : <></>}
<ScrollView>
{peerIds.map((value, index) => {
return <Text key={index}>{usernames[value + '']}</Text>;
})}
</ScrollView>
<Text style={styles.subHeading}>Audience List</Text>
{!isHost ? <Text>{myUsername}</Text> : <></>}
<ScrollView>
{Object.keys(usernames).map((key, index) => {
return (
<Text key={index}>
{peerIds.includes(parseInt(key, 10)) ? null : usernames[key]}
</Text>
);
})}
</ScrollView>
</View>
) : null;
};
}
...
我们定义了渲染函数,用于显示开始和结束调用的按钮以及切换角色。我们定义了一个函数 _renderUsers
用于渲染所有直播和观众成员的列表。
权限
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)
}
}
我们正在导出一个辅助函数来向Android操作系统申请麦克风权限。
样式
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
max: {
flex: 1,
// marginVertical: 40,
backgroundColor: '#F7F7F7',
},
buttonHolder: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-evenly',
},
button: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#38373A',
// borderRadius: 24,
},
buttonRed: {
paddingHorizontal: 16,
paddingVertical: 8,
// backgroundColor: '#F4061D',
borderRadius: 24,
},
buttonGreen: {
paddingHorizontal: 16,
paddingVertical: 8,
// backgroundColor: '#09DF18',
borderRadius: 24,
},
buttonText: {
color: '#fff',
},
fullView: {
flex: 5,
alignContent: 'center',
marginHorizontal: 24,
},
centerText: {
textAlign: 'center',
},
subHeading: {
fontSize: 16,
fontWeight: '700',
},
remote: {
width: 150,
height: 150,
marginHorizontal: 2.5,
},
noUserText: {
paddingHorizontal: 10,
paddingVertical: 5,
color: '#0093E9',
},
roleText: {
textAlign: 'center',
// fontWeight: '700',
color: '#fbfbfb',
fontSize: 18,
},
roleTextBold: {
textAlign: 'center',
fontSize: 18,
fontWeight: '700',
},
roleTextRed: {
textAlign: 'center',
fontSize: 18,
// color: '#F4061D',
},
spacer: {
width: '100%',
padding: '2%',
marginBottom: 32,
// borderWidth: 1,
backgroundColor: '#38373A',
color:'#fbfbfb',
// borderColor: '#38373A',
},
input: {
height: 40,
borderColor: '#38373A',
borderWidth: 1.5,
width: '90%',
alignSelf: 'center',
padding: 10,
},
errorText: { textAlign: 'center', margin: 5, color: '#38373A' },
});
Style.ts文件包含了组件的样式。
结论
这就是构建一个实时音视频直播App的简单方法。你可以参考声网React Native API参考中的方法,这些方法可以帮助你快速添加功能,如静音麦克风、设置音频配置文件、音频混合等。
获取更多文档、Demo、技术帮助
- 获取 SDK 开发文档,可访问声网文档中心。
- 如需参考各类场景 Demo,可访问下载中心获取。
- 如遇开发疑难,可访问论坛发帖提问。
- 了解更多教程、RTE 技术干货与技术活动,可访问声网开发者社区。
- 欢迎扫码关注我们。