开发最佳实践|集成声网 iOS SDK,实现语音聊天室

news2024/12/24 22:13:29

大家好,我是声网 RTE 开发者社区作者 @小曾同学。本次主要分享集成声网SDK实现语音聊天室。

01 前言

在日常生活中经常会看到一些聊天场景,比如在线KTV、连麦开黑、多人相亲、娱乐聊天室等应用场景,随着移动应用开发的需求不断增加,多人语音聊天室成为了一个热门的应用领域。那么聊天室该如何实现呢?你是想从0到1,还是集成第三方SDK呢?答案当然是集成第三方SDK,那么我们这篇文章就来教大家集成声网SDK实现一个语音聊天室Demo。

02 设计思路

  1. 在做产品之前需要明确需求,本次需求:实现语音聊天室Demo;

  2. 在确定需求之后,还需要对音视频这块有一定的了解,可以参考声网官网提供的音视频时序图,本次我们要实现的是多人语聊房,实现原理可以参考音视频的实现,音频通话不区分主播和观众,所有用户都是主播角色。

  1. 了解上述逻辑之后,设计Demo原型图,一个聊天室的构成基本上包含:输入房间名、加房、麦克风、用户界面等。我们本次主要实现一个简易聊天室demo,用户输入房间加入房间后,即可和远端用户保持通话,并可mute/unmute本地麦克风。设计如下

另外,在实现demo之前你需要一些准备工作,可参见【开发环境】。

03 开发环境

  • 开发平台:MacBook Pro
  • 编译工具:Xcode(14.2)
  • 真机:iPhone13(15.4.1)
  • Agora SDK:4.1.1

另外,你需要获取声网SDK、声网appID、Token等信息,具体获取方式可以参考官方文档。如果你还没有声网账号,可以通过这里免费注册,本次Demo使用的是SDK4.1.1版本,具体下载可查看SDK下载页面。

04 项目设置

1. 创建项目,集成声网SDK

项目名为:VoiceChatDemo,打开终端,进入根目录VoiceChatDemo下,输入命令pod init,该命令生成Podfile文件,并在Podfile文件中,输入pod 'AgoraRtcEngine_iOS','4.1.1’,表示集成声网sdk。之后在终端中输入命令pod install,表示下载依赖。

2. 添加媒体设备权限

本次实现的语聊房Demo,所以只需要给予麦克风权限

05 客户端实现

1. 加房页面创建

本次语音聊天室 Demo 主要涉及两个页面,一是用户加房页面 ViewController ,二是用户聊天室页面 RoomController 。而在RoomController中包含一个UICollection View,用于展示远端用户视图。

用户加房页面主要涉及5个内容,分别是 appidtokenchanneluid、加入房间。如果你还不知道如何获取声网appid等信息,可以参考官方文档。具体代码如下:

import UIKit
class ViewController: UIViewController {
    @IBOutlet weak var appidTF: UITextField!
    @IBOutlet weak var tokenTF: UITextField!
    @IBOutlet weak var uidTF: UITextField!
    @IBOutlet weak var roomTF: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
   }
}

加房跳转逻辑在页面中设计,具体如下图

当用户点击加入房间button时,会自动跳转到用户聊天室页面,这种方式称为Segue,表示从一种场景转换到另外一种场景中。

2. 聊天室实现逻辑

RoomController.swift文件中,实现聊天室逻辑功能

2.1 初始化操作

1)导入Agora SDK

import AgoraRtcKit
//自 3.0.0 版本起,AgoraRtcEngineKit 类名更换为 AgoraRtcKit

2)初始化声网引擎

// 初始化AgoraRtcEngineKit,可加入自定义配置,比如加入频道是否开启麦克风、摄像头等。
let config = AgoraRtcEngineConfig()
config.appId = appid
config.channelProfile = profile
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
agoraKit.enableAudio()
agoraKit.disableVideo()
agoraKit.enableAudioVolumeIndication(200, smooth: 3, reportVad: true)
//默认加入频道即发送音频,不发送视频
let option = AgoraRtcChannelMediaOptions()
option.publishCameraTrack = false
option.publishMicrophoneTrack = true
option.enableAudioRecordingOrPlayout = true
option.clientRoleType = .broadcaster
option.autoSubscribeAudio = true
2.2 聊天室页面设计

1)定义UserList

如下图,定义一个Collection View来渲染远端用户,名为User List,当远端用户加房时,UICollection View中就会增加一个自定义的Cell。

自定义的Cell的nib文件需要和RoomCell关联。

在使用Cell前,需要注册自定义的Cell到UICollection View中。

var nibName = UINib(nibName: "RoomCell", bundle:nil)
userList.register(nibName, forCellWithReuseIdentifier: "RoomCell")

2)数据绑定

那么,怎么把自定义的Cell 显示在Collection View里面,也就是说当远端用户加房时是怎么显示在页面上的?

Collection View 中有两个属性,一是dataSource,二是delegate其中,dataSource 表示数据来源,delegate 表示操作 Cell 的时候,一些事件委托谁来处理。

userList.delegate = self
userList.dataSource = self

那么dataSource是怎么绑定数据的呢?当远端用户加房后,怎么显示在界面上?

数据源是userArrayuserArray是远端用户的列表,当远端用户加入房间时会传入参数 uid,并将 uid 存到userArray数组中,当远端离开房间时,会调用remove()方法,将用户uid移除,在此过程中,控件需要重新刷新用户列表,即`userList.reloadData()``,将用户视图实时更新。

//远端用户加入房间
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
        let length = userArray.count
        if length == 0 {
            userArray.insert(Int(uid), at: 0)
        }else {
            userArray.insert(Int(uid), at: length-1 )
        }
        userList.reloadData()
    }
//远端用户离开房间
func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
        var indexNum : Int = 0
        for (index,value) in userArray.enumerated() {
            if value == uid {
                indexNum = index
            }
        }
        userArray.remove(at: indexNum)
        userList.reloadData()
    }

另外,当用户加入房间后,用户的uid是怎么显示在界面上的呢?numberOfItemsInSection 表示区域内有多少个item(元素),也就是表示数组的个数,如果数组为5,那么就会返回5给collectionView;每一个cell 都可自己定义,有几个元素,那么第二个方法就会调用几次。而当用户进来时,显示用户uid,即 cell.uidLabel.text = “(userArray[indexPath.row])”

extension RoomController : UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return userArray.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoomCell", for: indexPath) as! RoomCell
        cell.uidLabel.text = "\(userArray[indexPath.row])"
        return cell
    }

3)mute/unmute实现

当用户点击开启麦克风按钮时,会调用muteLocalAudioStream(false) 方法,当点击关闭麦克风时,参数时 true。

//打开麦克风
@IBAction func openMic(_ sender: UIButton) {
    agoraKit.muteLocalAudioStream(false)
}
//关闭麦克风
@IBAction func closeMic(_ sender: UIButton) {
    agoraKit.muteLocalAudioStream(true)
}
2.3 完整代码如下

import UIKit
import AgoraRtcKit
class RoomController: UIViewController, UICollectionViewDelegate {
    // 初始化操作
    var isJoined : Bool = false
    var agoraKit : AgoraRtcEngineKit!
    var appid,token,roomid : String!
    var uid : Int32 = 0
    var profile:AgoraChannelProfile = .liveBroadcasting
    //用户列表
    var userArray = [Int]()
    @IBOutlet weak var userList: UICollectionView!
    //展示用户列表
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setUp()
    }
    func setUp() {
        //初始化
        userList.delegate = self
        userList.dataSource = self
        appid = "afe...7063"
        token = nil
        uid = 0
        roomid = "zeng"
        let nibName = UINib(nibName: "RoomCell", bundle:nil)
        userList.register(nibName, forCellWithReuseIdentifier: "RoomCell")
        let config = AgoraRtcEngineConfig()
        config.appId = appid
        config.channelProfile = profile
        agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
        agoraKit.enableAudio()
        agoraKit.disableVideo()
        agoraKit.enableAudioVolumeIndication(200, smooth: 3, reportVad: true)
        let option = AgoraRtcChannelMediaOptions()
        option.publishCameraTrack = false
        option.publishMicrophoneTrack = true
        option.enableAudioRecordingOrPlayout = true
        option.clientRoleType = .broadcaster
        option.autoSubscribeAudio = true
        let result = agoraKit.joinChannel(byToken: token, channelId: roomid, uid: UInt(uid), mediaOptions: option)
        if result != 0 {
            self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params")
        } else {
            print("joinChannel successed!")
            self.showAlert(title: "Info", message: "joinChannel successed!")
        }
    }
    //打开麦克风
    @IBAction func openMic(_ sender: UIButton) {
        agoraKit.muteLocalAudioStream(false)
    }
    //关闭麦克风
    @IBAction func closeMic(_ sender: UIButton) {
        agoraKit.muteLocalAudioStream(true)
    }
    //显示用户列表界面
    @IBAction func showUserList(_ sender: Any) {
    }
    @IBAction func leaveRoom(_ sender: Any) {
        dismiss(animated: true)
    }
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        if isJoined {
            agoraKit.leaveChannel()
        }
    }
    func showAlert(title: String? = nil, message: String, textAlignment: NSTextAlignment = .center) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
        alertController.addAction(action)
        if let messageLabel = alertController.view.value(forKeyPath: "_messageLabel") as? UILabel {
            messageLabel.textAlignment = textAlignment
        }
        self.present(alertController, animated: true, completion: nil)
    }
}
///AgoraRtcEngineDelegate
extension RoomController : AgoraRtcEngineDelegate {
    func rtcEngine(_ engine: AgoraRtcEngineKit, didOccur errorType: AgoraEncryptionErrorType) {
        self.showAlert(title: "Error", message: "didOccur: \(errorType), please check your params")
    }
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
//        self.showAlert(title: "Info", message: "didJoinedOfUid: \(uid)")
        let length = userArray.count
        if length == 0 {
            userArray.insert(Int(uid), at: 0)
        }else {
            userArray.insert(Int(uid), at: length-1 )
        }
        userList.reloadData()
    }
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
        self.isJoined = true
    }
    func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
        var indexNum : Int = 0
        for (index,value) in userArray.enumerated() {
            if value == uid {
                indexNum = index
            }
        }
        userArray.remove(at: indexNum)
        userList.reloadData()
    }
}
///UITableViewDataSource && UITableViewDelegate
extension RoomController : UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return userArray.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoomCell", for: indexPath) as! RoomCell
        cell.uidLabel.text = "\(userArray[indexPath.row])"
        return cell
    }
}
}

06 Demo展示

07 小结

本次主要基于声网SDK实现iOS聊天室,功能比较简易,如果你想丰富自己的Demo,想要模拟一些场景,比如在线狼人杀、在线KTV、音效聊天室等,声网提供了非常丰富的API,会使语聊房更加沉浸、更加有趣、更加好听。主旨是打造一种“声临其境”的互动玩法,更多信息可参考声网的声动语聊。

————————

  • 欢迎注册声网帐号,领取每月 10000 分钟免费使用额度(注册流程参考)
  • 下载声网相关 SDK & Demo,体验四行代码、三十分钟快速构建沉浸式实时互动场景
  • 交流提问 & 撰写文章,更好的技术氛围可访问「声网 RTE 开发者社区」

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/562481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

提高记忆力的 10 种记忆技巧和工具

人工智能的迅速发展,让人不禁猜想是否有一天我们也能靠AI来提高自己的记忆力?哈哈也许真的可行🤖 其实现在已经有很多记忆技巧和工具,不仅可以帮助我们存储信息,还可以更好地回忆和记忆更大的信息集。 实际上&#x…

lwIP更新记07:TCP 控制块申请失败可以检测到了

从 lwIP-2.0.0 开始,TCP 控制块申请失败可以检测到了。 这个更新应用在 TCP 服务器模式中,处于监听状态的 TCP_PCB ,如果收到客户端发送的 SYN 同步标志,表示一个客户端在请求建立连接了。lwIP 会为这个新连接申请一个 TCP_PCB &…

APP启动页和闪屏的知识点

APP启动页和闪屏的知识点_51CTO博客_app启动屏广告 启动页与闪屏的区别 1.启动页 (launch screen ) 当app被用户打开时,在app启动过程中被用户所看到的过渡页面(或动画)都被我们统称为启动页。 优点: 1.打开一个产品时,需要有一定的时间加载&#xff…

windows sever服务器安装系统新手版

混在learnsite群里已经有几年了吧,但是只有一段时间用了别人现成的一个iso版本还比较老,这次打算自己弄一个,一开始打算linux下弄,结果系统都装完了发现人家说learnsite在linux下有点bug没解决好,只能win下再弄&#x…

爬虫为什么会使用到代理ip?

爬虫使用代理IP的主要目的是为了隐藏自己的真实IP地址,以避免被目标网站封禁或限制访问。如果一个爬虫频繁地向一个网站发送请求,而且每次请求的IP地址都相同,那么这个网站就有可能认为这是一种恶意行为,从而采取封禁或限制访问的…

用 Python 写 3D 游戏

vizard介绍 Vizard是一款虚拟现实开发平台软件,从开发至今已走过十个年头。它基于C/C,运用新近OpenGL拓展模块开发出的高性能图形引擎。当运用Python语言执行开发时,Vizard同时自动将编写的程式转换为字节码抽象层(LAXMI),进而运行…

如何对项目进度进行跟踪?逐步完善项目计划

我接手了一个小项目,但是无论是我还是领导,都认为这是个简单的项目,最多一月时间就能搞定。但是,随着时间推移,三个月也没有将内容完善。于是我进行了反思总结,我认为存在如下问题: 1、资源协…

vue3在setup中请求数据并使用的几种方式

因为Composition组合式API setup有一点点不同特此举例几种可行的请求数据并使用方式 第一种 Promise 参考代码如下 <template><div>{{ min }}</div> </template><script> import { ref } from vue; import { getUser } from /api/user export d…

应急响应之内存分析方法

应急响应之内存分析方法 1.内存的获取基于内核模式程序的内存获取基于系统崩溃转储的内存获取基于虚拟化快照的内存获取dumpit获取(推荐)2.内存的分析RedlineVolatility1.内存的获取 基于内核模式程序的内存获取 这种获取方法一般需要借助相关的工具来完成。常用的提取工具…

leetcode 2542. Maximum Subsequence Score(最大子串分数)

2个数组&#xff0c;长度一样&#xff0c;从中选k个下标&#xff08;两个数组用同样的下标&#xff09;&#xff0c; 会得到k个nums1中的数字&#xff0c;和k个nums2中的数字。 score k个nums1的数字之和 ✖ min(k个nums2的数字&#xff09;&#xff0c; 找到最大的score。 思…

【Qt】QLocalSocket与QLocalServer问题:接收不到数据、只能收到第一条、数据不完整解决方案【2023.05.24】

简介 Qt很强大,但是Qt的帮助文档、API属实是让我们走不少弯路。QLocalSocket一个很简单的东西,我仅想用来实现一个简单的本地进程通信,就遇到了:客户端循环发送数据,服务端只能接收到一条、接收到数据不完整等奇奇怪怪的现象。 最郁闷的是,网上很多教程说的都是错的😒。…

Web服务器实现|基于阻塞队列线程池的Http服务器|线程控制|Http协议

基于阻塞队列生产者消费者模型线程池的多线程Web服务器 代码地址&#xff1a;WebServer_GitHub_Addr README 摘要 本实验通过C语言&#xff0c;实现了一个基于阻塞队列线程池的多线程Web服务器。该服务器支持通过http协议发送报文&#xff0c;跨主机抓取服务器上特定资源。与…

火山引擎数智平台VeDI助力某办公软件企业营销线索转化提升14%

一条营销线索&#xff0c;从官网后台下载到完成成交&#xff0c;到底需要经历哪些环节&#xff1f; 在企业级市场的销售场景中&#xff0c;营销线索通常是指用户通过相关产品的官方网站或者营销活动界面&#xff0c;主动留下的联系方式&#xff1b;而根据线索价值的不同&#…

2023京东618全民拆快递互动活动玩法规则!

2023京东618全民拆快递&#xff0c;瓜分20亿活动规则&#xff01; 618无门槛红包29号开领&#xff01; ​手机京东搜索&#xff1a;好运红包210&#xff0c;领最高20618&#xff0c;每天可领三次&#xff01; ​手机京东搜索&#xff1a;能省就省50&#xff0c;领最高23888…

深度学习进阶篇-预训练模型[1]:预训练分词Subword、ELMo、Transformer模型原理;结构;技巧以及应用详解

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

算法设计与分析期末总结

前言&#xff1a;基本是为了我自己看的一些我容易忘记的东西&#xff0c;为考试作准备把&#xff0c;主要使后半部分的知识&#xff0c;前半部分请看算法设计与分析阶段考总结 第五章 回溯算法是一种系统地搜索问题的解的方法。某个问题的所有可能解的称为问题的解空间&#xf…

百度工程师移动开发避坑指南——Swift语言篇

作者 | 启明星小组 上一篇我们介绍了移动开发常见的内存泄漏问题&#xff0c;见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。 对于Swift开发者&#xff0c;Swift较于OC一个很大的不同就是引入了可选类型&#xff08;Optional&#…

Install Redis Cluster(1master-2slave) on Kubernetes

目录 Node & Software & Docker Images Lists Prerequisites Architecture Setting up your Redis cluster Creating Namespace Creating StorageClass Creating Persistent volumes Creating ConfigMap Creating StatefulSet Creating Headless Service …

中创|警惕AI骗局,10分钟被骗430万,AI诈骗正在全国爆发!

眼见为实&#xff1f;耳听为真&#xff1f;当心AI诈骗&#xff01; 只需要提供一张带脸的照片&#xff0c;就可以把自己置换成视频、电视剧中的男&#xff08;女&#xff09;主角&#xff0c;拟真度非常高&#xff0c;毫无违和感&#xff0c;这是最近爆火的AI换脸。 然而随着人…

浏览器数据存储方式

浏览器数据存储方式 常用的前端数据存储方法笼统来说有 3 种&#xff1a; local/session storagecookiesindexeddb 3 种方法各有各的优点和使用范围。 local/session storage local/session storage 保存的格式都为键值对&#xff0c;并且用法都是差不多&#xff0c;如下&…