使用 Agora SDK 开发 React Native 视频通话App

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

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

我们将使用 Agora RTC SDK for React Native来做例子。在这篇文章中,我使用的版本是 v3.1.6。下载最新 SDK,可访问声网官网:https://docs.agora.io/cn/All/downloads?platform=All%20Platforms

创建一个Agora账户

声网后台中的项目管理
  • 找到 "项目管理 "下的 "项目列表 "选项卡,点击蓝色的 "创建 "按钮,创建一个项目。(当提示使用 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 开发者
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容