在 React Native 的应用中,从头开始添加视频通话功能是很复杂的。要保证低延迟、负载平衡,还要注意管理用户事件状态,非常繁琐。除此之外,还必须保证跨平台的兼容性。

当然有个简单的方法可以做到这一点。在本次的教程中,我们将使用 Agora Video SDK 来构建一个 React Native 视频通话 App。在深入探讨程序工作之前,我们将介绍应用的结构、设置和执行。你可以在几分钟内,通过几个简单的步骤,让一个跨平台的视频通话应用运行起来。

我们将使用 Agora RTC SDK for React Native 来做例子。在这篇文章中,我使用的版本是 v3.1.6。

创建一个Agora账户

登录声网后台.png

  • 找到 "项目管理 "下的 "项目列表 "选项卡,点击蓝色的 "创建 "按钮,创建一个项目。(当提示使用 App ID+证书时,选择只使用 App ID。)记住你的 App ID,它将在开发App时用于授权你的请求。
注意:本文没有采用 Token 鉴权,建议在生产环境中运行的所有RTE App 都采用Token鉴权。有关Agora平台中基于Token的身份验证的更多信息,请在声网文档中心搜索关键词「Token」,参考相关文档。

示例项目结构

这就是我们正在构建的应用程序的结构:

.

├── android

├── components

│ └── Permission.ts

│ └── Style.ts

├── ios

├── App.tsx

.

让我们来运行这个应用

  • 需要安装 LTS 版本的 Node.js 和 NPM。
  • 确保你有一个 Agora 账户,设置一个项目,并生成 App ID。
  • 从主分支下载并解压 ZIP 文件。
  • 运行 npm install 来安装解压目录中的 App 依赖项。
  • 导航到 ./App.tsx,将我们之前生成的 App ID 填入 appId: "<YourAppId>"
  • 如果你是为 iOS 构建,打开终端,执行 cd ios && pod install
  • 连接你的设备,并运行 npx react-native run-android / npx react-native run-ios来启动应用程序。等待几分钟来构建和启动应用程序。
  • 一旦你看到手机(或模拟器)上的主屏幕,点击设备上的开始通话按钮。(iOS模拟器不支持摄像头,所以要用实体设备代替)。

通过以上操作,你应该可以在两个设备之间进行视频聊天通话。该应用默认使用 channel-x 作为频道名称。

应用工作原理

App.tsx

这个文件包含了 React Native 视频通话App中视频通话的所有核心逻辑。

import React, {Component} from 'react'
import {Platform, ScrollView, Text, TouchableOpacity, View} from 'react-native'
import RtcEngine, {RtcLocalView, RtcRemoteView, VideoRenderMode} from 'react-native-agora'

import requestCameraAndAudioPermission from './components/Permission'
import styles from './components/Style'

/**
 * @property peerIds Array for storing connected peers
 * @property appId
 * @property channelName Channel Name for the current session
 * @property joinSucceed State variable for storing success
 */
interface State {
    appId: string,
    token: string,
    channelName: string,
    joinSucceed: boolean,
    peerIds: number[],
}

...

我们开始先写import声明。接下来,为应用状态定义一个接口,包含:

  • appId:Agora App ID
  • token:为加入频道而生成的Token。
  • channelName:频道名称(同一频道的用户可以通话)。
  • joinSucceed:存储是否连接成功的布尔值。
  • peerIds:一个数组,用于存储通道中其他用户的UID。
...

export default class App extends Component<Props, State> {
    _engine?: RtcEngine

    constructor(props) {
        super(props)
        this.state = {
            appId: YourAppId,
            token: YourToken,
            channelName: 'channel-x',
            joinSucceed: false,
            peerIds: [],
        }
        if (Platform.OS === 'android') {
            // Request required permissions from Android
            requestCameraAndAudioPermission().then(() => {
                console.log('requested!')
            })
        }
    }

    componentDidMount() {
        this.init()
    }

    /**
     * @name init
     * @description Function to initialize the Rtc Engine, attach event listeners and actions
     */
    init = async () => {
        const {appId} = this.state
        this._engine = await RtcEngine.create(appId)
        await this._engine.enableVideo()

        this._engine.addListener('Warning', (warn) => {
            console.log('Warning', warn)
        })

        this._engine.addListener('Error', (err) => {
            console.log('Error', err)
        })

        this._engine.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._engine.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._engine.addListener('JoinChannelSuccess', (channel, uid, elapsed) => {
            console.log('JoinChannelSuccess', channel, uid, elapsed)
            // Set state variable to true
            this.setState({
                joinSucceed: true
            })
        })
    }

...

我们定义了一个基于类的组件:变量 _engine 将存储从 Agora SDK 导入的 RtcEngine 类实例。这个实例提供了主要的方法,我们的应用程序可以调用这些方法来使用SDK的功能。

在构造函数中,设置状态变量,并为 Android 上的摄像头和麦克风获取权限。(我们使用了下文所述的 permission.ts 的帮助函数)当组件被挂载时,我们调用 init 函数 ,使用 App ID 初始化 RTC 引擎。它还可以通过调用 engine 实例上的 enableVideo 方法来启用视频。(如果省略这一步,SDK 可以在纯音频模式下工作。)

init函数还为视频调用中的各种事件添加了事件监听器。例如,UserJoined 事件为我们提供了用户加入频道时的 UID。我们将这个 UID 存储在我们的状态中,以便在以后渲染他们的视频时使用。

注意:如果在我们加入之前有用户连接到频道,那么在他们加入频道之后,每个用户都会被触发一个 UserJoined 事件。
...
    /**
     * @name startCall
     * @description Function to start the call
     */
    startCall = async () => {
        // Join Channel using null token and channel name
        await this._engine?.joinChannel(this.state.token, this.state.channelName, null, 0)
    }

    /**
     * @name endCall
     * @description Function to end the call
     */
    endCall = async () => {
        await this._engine?.leaveChannel()
        this.setState({peerIds: [], joinSucceed: false})
    }

    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}>
                <RtcLocalView.SurfaceView
                    style={styles.max}
                    channelId={this.state.channelName}
                    renderMode={VideoRenderMode.Hidden}/>
                {this._renderRemoteVideos()}
            </View>
        ) : null
    }

    _renderRemoteVideos = () => {
        const {peerIds} = this.state
        return (
            <ScrollView
                style={styles.remoteContainer}
                contentContainerStyle={{paddingHorizontal: 2.5}}
                horizontal={true}>
                {peerIds.map((value, index, array) => {
                    return (
                        <RtcRemoteView.SurfaceView
                            style={styles.remote}
                            uid={value}
                            channelId={this.state.channelName}
                            renderMode={VideoRenderMode.Hidden}
                            zOrderMediaOverlay={true}/>
                    )
                })}
            </ScrollView>
        )
    }
}

接下来,还有开始和结束视频聊天通话的方法。 joinChannel 方法接收 Token、频道名、其他可选信息和一个可选的 UID(如果你将 UID 设置为 0,系统会自动为本地用户分配 UID)。

我们还定义了渲染方法,用于显示开始和结束通话的按钮,以及显示本地视频源和远程用户的视频源。我们定义了 _renderVideos 方法 来渲染我们的视频源,使用 peerIds 数组在滚动视图中渲染。

为了显示本地用户的视频源,我们使用 <RtcLocalView.SurfaceView> 组件,需要提供 channelIdrenderMode 。连接到同一 个 channelId 的用户可以相互通信 ,而 renderMode 用于将视频放入视图中或通过缩放来填充视图。

为了显示远程用户的视频源,我们使用 SDK 中的 <RtcLocalView.SurfaceView> 组件,它可以获取远程用户的 UID 以及 channelIdrenderMode

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)
    }
}

导出一个函数,向Android上的操作系统申请摄像头和麦克风的权限。

Style.ts

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,
        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 - 100,
    },
    remoteContainer: {
        width: '100%',
        height: 150,
        position: 'absolute',
        top: 5
    },
    remote: {
        width: 150,
        height: 150,
        marginHorizontal: 2.5
    },
    noUserText: {
        paddingHorizontal: 10,
        paddingVertical: 5,
        color: '#0093E9',
    },
})

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

这就是快速开发一个 React Native 视频聊天通话 App 的方法。你可以参考 Agora React Native API Reference 去查看可以帮助你快速添加更多功能的方法,比如将摄像头和麦克风静音,设置视频配置文件和音频混合等等。

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

声网 Agora 开发者


RTE开发者社区
647 声望966 粉丝

RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。