在React Native上使用Agora开发多频道音视频应用
自从Agora SDK for React Native v3.0.0发布后,现在用户可以同时加入无限数量的频道。但你同时只能向一个频道发布自己的摄像头视频流。
这种功能在多房间的情况下真的很方便,你既可以发送和接收主房间的音视频,同时也可以接收次要房间的音视频。
我们将使用Agora RTC SDK for React Native 作为我们的例子
在深入了解它的工作原理之前,我们先来看看几个关键点
-
我们将使用SDK连接到第一个频道并正常加入音视频通话。推送我们的视频流,同时也会接收该频道上其他用户的视频。
-
接下来,我们将加入第二个频道来接收该频道所有用户的直播。注意,频道2上的用户将无法接收我们的视频。
-
两个频道是分开的:频道1和频道2上的用户看不到对方。我们可以扩展此功能来加入所需的更多频道。
示例程序结构
这就是应用的结构:
.
├── android
├── components
│ └── Permission.ts
│ └── Style.ts
├── ios
├── App.tsx
.
下载源码
如果你想跳到代码中自己尝试一下,可以看看自述文件了解如何运行app的步骤。代码在GitHub上开源。此App使用channel-1和channel-2作为频道名称。
当你运行App时,你会看到两个按钮:一个是加入通话,一个是结束通话。当你点击开始通话时,你应该会在最上面一行看到你的视频,其中包含频道1的视频。而底行包含来自频道2的视频。
注意:本指南没有实现Token鉴权,建议所有在生产环境中运行的RTE应用都采用Token鉴权。有关Agora平台内基于Token鉴权的更多信息,请参考校验用户权限。
应用程序如何工作
App.tsx
App.tsx将是进入应用程序的入口。我们的所有代码都在这个文件中:
import React, { Component } from 'react';
import { Platform, ScrollView, Text, TouchableOpacity, View } from 'react-native';
import RtcEngine, { RtcChannel, RtcLocalView, RtcRemoteView, VideoRenderMode } from 'react-native-agora';
import requestCameraAndAudioPermission from './components/Permission';
import styles from './components/Style';
interface Props {}
/**
* @property appId Used to
* @property token Used to join a channel
* @property channelNameOne Channel Name for the current session
* @property channelNameTwo Second Channel Name for the current session
* @property joinSucceed State variable for storing success
* @property peerIdsOne Array for storing connected peers on first channel
* @property peerIdsTwo Array for storing connected peers on second channel
*/
interface State {
appId: string;
token: string | null;
channelNameOne: string;
channelNameTwo: string;
joinSucceed: boolean;
peerIdsOne: number[];
peerIdsTwo: number[];
}
我们先编写import声明。接下来,为我们的应用状态定义一个接口,包含以下内容:
-
appId: Agora App ID
-
token:为加入该频道而生成的Token
-
channelNameOne:频道1的名称
-
channelNameTwo: 频道2的名称
-
joinSucceed:如果我们连接成功,就用这个布尔值来存储
-
peerIdsOne: 数组,用于存储频道1中其他用户的UID
-
peerIdsTwo: 数组,用于存储频道2中其他用户的UID
... export default class App extends Component<Props, State> { _engine?: RtcEngine; _channel?: RtcChannel; constructor(props) { super(props); this.state = { appId: 'ENTER YOUR APP ID', token: null, //using token as null for App ID without certificate channelNameOne: 'channel-1', channelNameTwo: 'channel-2', joinSucceed: false, peerIdsOne: [], peerIdsTwo: [], }; if (Platform.OS === 'android') { // Request required permissions from Android requestCameraAndAudioPermission().then(() => { console.log('requested!'); }); } } componentDidMount() { this.init(); } componentWillUnmount() { this.destroy(); } ...
我们定义一个基于类的组件:_rtcEngine变量将存储RtcEngine类的实例,_channel变量将存储RtcChannel类的实例,我们可以用它来访问SDK函数。
在构造函数中,设置我们的状态变量,并申请在Android上录制音频的权限。(使用权限中的帮助函数,如下所述)。当组件被挂载时,调用init函数,它初始化RTC引擎和RTC频道。当组件卸载时,销毁我们的引擎和频道实例。
RTC初始化
...
/**
* @name init
* @description Function to initialize the Rtc Engine, attach event listeners and actions
*/
init = async () => {
const { appId, channelNameTwo } = this.state;
this._engine = await RtcEngine.create(appId);
this._channel = await RtcChannel.create(channelNameTwo);
await this._engine.enableVideo();
this._engine.addListener('Error', (err) => {
console.log('Error', err);
});
this._channel.addListener('Error', (err) => {
console.log('Error', err);
});
this._engine.addListener('UserJoined', (uid, elapsed) => {
console.log('UserJoined', uid, elapsed);
// Get current peer IDs
const { peerIdsOne } = this.state;
// If new user
if (peerIdsOne.indexOf(uid) === -1) {
this.setState({
// Add peer ID to state array
peerIdsOne: [...peerIdsOne, uid],
});
}
});
this._engine.addListener('UserOffline', (uid, reason) => {
console.log('UserOffline', uid, reason);
const { peerIdsOne } = this.state;
this.setState({
// Remove peer ID from state array one
peerIdsOne: peerIdsOne.filter((id) => id !== uid),
});
});
this._channel.addListener('UserJoined', (uid, elapsed) => {
console.log('UserJoined', uid, elapsed);
// Get current peer IDs
const { peerIdsTwo } = this.state;
// If new user
if (peerIdsTwo.indexOf(uid) === -1) {
this.setState({
// Add peer ID to state array
peerIdsTwo: [...peerIdsTwo, uid],
});
}
});
this._channel.addListener('UserOffline', (uid, reason) => {
console.log('UserOffline', uid, reason);
const { peerIdsTwo } = this.state;
this.setState({
// Remove peer ID from state array two
peerIdsTwo: peerIdsTwo.filter((id) => id !== uid),
});
});
// If Local user joins RTC channel
this._channel.addListener('JoinChannelSuccess', (channel, uid, elapsed) => {
console.log('JoinChannelSuccess', channel, uid, elapsed);
// Set state variable to true
this.setState({
joinSucceed: true,
});
});
};
...
我们使用App ID来创建我们的引擎实例。该引擎实例将用于连接到频道1,在那里我们同时发送和接收视频。我们还使用第二个频道的名称来创建我们的频道实例。频道实例将仅用于接收来自频道2的视频。
当我们加入频道时,RTC会对每个在场的用户以及之后加入的每个新用户触发一个userJoined事件。当用户离开频道时,会触发userOffline事件。我们在 _engine 和 _channel 上使用事件监听器来存储和维护 peerIdsOne 和 peerIdsTwo 数组,这两个数组中包含了两个频道中用户的 UID。
我们还为joinChannelSuccess附加了一个监听器来更新我们的状态变量,这个状态变量是在调用中时用来渲染UI的。
我们按钮的功能
...
/**
* @name startCall
* @description Function to start the call
*/
startCall = async () => {
// channelOptions object used to auto subscribe to remote streams on second channel
let channelOptions = {
autoSubscribeAudio: true,
autoSubscribeVideo: true,
};
// Join Channel One using RtcEngine object, null token and channel name and UID as 0 to have the SDK auto generate it
await this._engine?.joinChannel(
this.state.token,
this.state.channelNameOne,
null,
0
);
// Join Channel Two using RtcChannel object, null token, uid as 0, channel name and channelOptions object
await this._channel?.joinChannel(this.state.token, null, 0, channelOptions);
};
/**
* @name endCall
* @description Function to end the call by leaving both channels
*/
endCall = async () => {
await this._engine?.leaveChannel();
await this._channel?.leaveChannel();
this.setState({ peerIdsOne: [], peerIdsTwo: [], joinSucceed: false });
};
/**
* @name destroy
* @description Function to destroy the RtcEngine and RtcChannel instances
*/
destroy = async () => {
await this._channel?.destroy();
await this._engine?.destroy();
};
...
startCall
函数使用joinChannel方法加入两个频道。
endCall
函数使用leaveChannel方法离开两个频道并更新状态。
destroy
函数销毁了我们的引擎和频道的实例。
渲染用户界面
...
render() {
return (
<View style={styles.max}>
<View style={styles.max}>
<View style={styles.buttonHolder}>
<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>
{this._renderVideos()}
</View>
</View>
);
}
_renderVideos = () => {
const { joinSucceed } = this.state;
return joinSucceed ? (
<View style={styles.fullView}>
{this._renderRemoteVideosOne()}
{this._renderRemoteVideosTwo()}
</View>
) : null;
};
_renderRemoteVideosOne = () => {
const { peerIdsOne } = this.state;
return (
<ScrollView
style={styles.scrollHolder}
contentContainerStyle={styles.scrollView}
horizontal={true}
>
<RtcLocalView.SurfaceView
style={styles.remote}
channelId={this.state.channelNameOne}
renderMode={VideoRenderMode.Hidden}
/>
{peerIdsOne.map((value) => {
return (
<RtcRemoteView.SurfaceView
style={styles.remote}
uid={value}
channelId={this.state.channelNameOne}
renderMode={VideoRenderMode.Hidden}
zOrderMediaOverlay={true}
key={value}
/>
);
})}
</ScrollView>
);
};
_renderRemoteVideosTwo = () => {
const { peerIdsTwo } = this.state;
return (
<ScrollView
style={styles.scrollHolder}
contentContainerStyle={styles.scrollView}
horizontal={true}
>
{peerIdsTwo.map((value) => {
return (
<RtcRemoteView.SurfaceView
style={styles.remote}
uid={value}
channelId={this.state.channelNameTwo}
renderMode={VideoRenderMode.Hidden}
zOrderMediaOverlay={true}
key={value}
/>
);
})}
</ScrollView>
);
};
}
我们定义了渲染函数,用于显示开始和结束通话的按钮,并显示两个频道的用户视频。
我们定义了一个_renderVideos函数来渲染我们两个频道中使用频道1和频道2的_renderRemoteVideosOne和_renderRemoteVideosTwo函数的视频,每个函数都包含scrollViews用来显示频道的音视频。我们使用存储在peerId数组中的UID,通过传递给RtcRemoteView.SurfaceView组件来渲染远程用户的视频。
权限
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 { Dimensions, StyleSheet } from 'react-native';
const dimensions = {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
};
export default StyleSheet.create({
max: {
flex: 1,
},
buttonHolder: {
height: 100,
marginVertical: 15,
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-evenly',
},
button: {
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: '#0093E9',
borderRadius: 25,
},
buttonText: {
color: '#fff',
},
fullView: {
width: dimensions.width,
height: dimensions.height - 130,
},
remote: {
width: (dimensions.height - 150) / 2,
height: (dimensions.height - 150) / 2,
marginHorizontal: 2.5,
},
scrollView: {
paddingHorizontal: 2.5, justifyContent: 'center', alignItems: 'center'
},
scrollHolder: { flex: 1, borderWidth: 1 },
});
Style.ts
文件包含了组件的样式。
结论
这就是我们如何构建一个可以同时连接两个频道的音视频通话App的方法。你可以通过React Native API 参考来查看可以帮助你快速添加其他功能的方法,比如静音麦克风、设置音频配置文件和音频混合。
获取更多文档、Demo、技术帮助
- 获取 SDK 开发文档,可访问声网文档中心。
- 如需参考各类场景 Demo,可访问下载中心获取。
- 如遇开发疑难,可访问论坛发帖提问。
- 了解更多教程、RTE 技术干货与技术活动,可访问声网开发者社区。
- 欢迎扫码关注我们。