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

news2024/11/30 15:28:30

😄作者简介:
小曾同学.com,一个致力于测试开发的博主⛽️,
如果文章知识点有错误的地方,还请大家指正,让我们一起学习,一起进步。😊
座右铭:不想当开发的测试,不是一个好测试✌️。
如果感觉博主的文章还不错的话,还请点赞、收藏哦!👍

文章目录

  • 一、前言
  • 二、设计思路
  • 三、开发环境
  • 四、项目设置
    • 1. 创建项目,集成声网SDK
    • 2. 添加媒体设备权限
  • 五、客户端实现
    • 1. 加房页面创建
    • 2. 聊天室实现逻辑
      • 2.1 初始化操作
      • 2.2 聊天室页面设计
      • 2.3 完整代码如下
  • 六、Demo展示
  • 七、总结

一、前言

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

二、设计思路

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

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

    在这里插入图片描述

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

    在这里插入图片描述

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

三、开发环境

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

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

四、项目设置

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

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

在这里插入图片描述

2. 添加媒体设备权限

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

在这里插入图片描述

五、客户端实现

1. 加房页面创建

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

用户加房页面主要涉及5个内容,分别是 appid、token、channel、uid、加入房间。如果你还不知道如何获取声网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,当用户加房时,页面中就会增加一个自定义的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是怎么绑定数据的呢?当远端用户加房后,怎么显示在界面上?

数据源是userArray,userArray是远端用户的列表,当远端用户加入房间时会传入参数 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
    }
}

    
}

六、Demo展示

在这里插入图片描述

七、总结

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

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

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

相关文章

初探Sharding-JDBC订单表分片实现

设计订单系统有两个数据库db_order_01和db_order_02。每个数据库分别有t_order_0和t_order_1两张订单表。 订单表设计有订单ID(order_id),用户ID(user_id),商户ID(merchant_id)。假设商户查看订单操作要比用户查看订单的操作更加频繁。避免商户查询订单时…

NLP学习笔记五-simple RNN

NLP学习笔记五-simple RNN 我这个学习笔记,感兴趣的小伙伴,看的时候尽量从头开始看,这样更好理解,也更有收获。 simple RNN的单元结构图如下: 其中A就是我们需要学习的参数矩阵, h t − 1 h_{t-1} ht−1​…

【MySQL数据库 | 第十五篇】事务

目录 前言: 介绍事务: 控制事务: 事务四大特性: 并发事务问题: 事务隔离级别: 总结: 前言: 这章我们将进入到MySQL基础篇的最后一章:事务,希望大家可以坚持下去&#xf…

车间如何做好“生产计划”,打造高效运营的智能工厂

新形势下,面对外部不断变化的市场需求、供应链下游企业管理升级需求以及持续上涨的人力成本,传统工厂模式必须要变革才能更好地发展。热潮之下,企业纷纷规划建设智能工厂。那么,新工厂规划如何避免投入浪费,少走弯路&a…

Linux——进程间通信,信号量的使用+小demo(C语言)

一.什么是信号量呢?用途又是什么呢? 信号量就是解决进程之间竞争资源的情况,比如:我们在宿舍用的公共洗衣机,我们只有当它空闲的时候,我们才可以去使用它,当别人看到洗衣机在使用的时候&#xf…

LVS负载均衡群集部署(DR模式)

一.DR模式 LVS负载均衡群集部署 ipvsadm 工具选项说明: 工具选项作用-A添加虚拟服务器-D删除整个虚拟服务器-s指定负载调度算法(轮询:rr、加权轮询:wrr、最少连接:lc、加权最少连接:wlc)-a表示…

clickhouse-MergeTree

创建建表语句 create table t_order_mt(id UInt32,sku_id String,total_amount Decimal(16,2),create_time Datetime ) engine MergeTreepartition by toYYYYMMDD(create_time)primary key (id)order by (id,sku_id); 插入测试数据 insert into t_order_mt(id,sku_id,total_…

chatgpt赋能python:Python数据分析必备工具:Pandas

Python数据分析必备工具:Pandas Python作为一门流行的编程语言,广泛应用于数据科学领域。而Pandas作为Python语言下的数据分析库,被广泛地应用于数据处理、数据分析、数据可视化等方面。本文将介绍如何快速地入门Pandas并进行数据分析。 什…

《统计学习方法》——逻辑斯谛回归与最大熵模型(下)

最大熵模型 极大似然估计 下面证明对偶函数的极大化等价于最大熵模型的极大似然估计。 极大似然估计的思想就是通过概率最大化来求出最符合的分类。对应的步骤为: 根据训练集,写出不同分类下的概率函数将不同分类下的概率函数进行汇总,写…

[hsctf 2023] crypto,pwn,rev部分

刚完了天津又来个衡水,这个大部分题比较简单,最后两天整了3个crypto有点意思. crypto double-trouble 给了密文 Hvwg gvcizr bch ps hcc vofr. Wb toqh, W kwzz uwjs wh hc mci fwuvh bck! Hvs tzou wg hvs tczzckwbu: OmqemdOubtqdeMdqOaax Vcksjsf, wh wg sbqcrsr gc mci …

刷脸登录(人工智能)

刷脸登录 理解刷脸登录的需求 理解刷脸登录的开发流程实现刷脸登录功能 浅谈人工智能 人工智能的概述 人工智能(Artificial Intelligence),英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门…

chatgpt赋能python:Python怎么下pip:简单又方便的工具

Python怎么下pip:简单又方便的工具 Python 是一门广泛应用的高级编程语言,它设计的初衷是让程序员更加愉悦地编写代码,同时提供了多种强大的库和框架。其中,pip 是最为常见的第三方包管理工具,本文将为你介绍如何下载…

uniapp实现主题切换功能实现第一种方式(scss变量+vuex)

随着用户端体验的不断提升,很多应用在上线的时候都要求做不同的主题,最基本的就是白天与夜间主题。 就像b站app主题切换,像这样的 uniapp因为能轻松实现多端发布而得到很多开发者的青睐,但每个端的实现也有可能不同,现…

NLP学习笔记四-word embeding

NLP学习笔记四-word embeding word embeding就是一块重头戏了,因为这里做完,我们的数据处理部分也基本上收尾了。 下面我们附上一张图: 如上图,word embeding实在我们one-hot word之后才可以进行的,每一步处理技术都是…

NVM安装(管理Node.js版本)

NVM可以在一台电脑上安装多个版本Node.js,并且可以一条指令随时下载或切换版本 下载安装 github下载地址:https://github.com/coreybutler/nvm-windows/releases 一路next,注意修改安装路径即可(不要安装在有中文名称的目录下&am…

Latex图表制作:关于EPS文件转PDF文件及PDF裁剪自动化的Shell脚本制作

关于EPS文件转PDF文件及PDF裁剪自动化的Shell脚本制作 前言一、EPS文件转PDF文件1. 过去的科研绘图方案2. 未来的科研绘图方案 二、使用步骤1. 单例操作a. 安装epstoolb. 切除EPS文件白边并保存临时EPS文件d. 将EPS文件转换为PDF文件 2. 批处理操作a. Shell 脚本b. 使用说明 参…

贝叶斯公式的理解与推导(简单版本)

院子里面有条狗,过去几十年中,平均100天狗会叫50天,平均100天被偷盗一次,偷盗时有90%概率狗会叫,请问当狗叫的时候,是小偷入侵的概率有多大? 分析: 狗叫为独立事件A,偷盗…

chatgpt赋能python:Python如何将PDF转换为Word文档

Python如何将PDF转换为Word文档 在现代电脑使用的日常工作中,PDF格式的文档已经变得很普遍。这种文件格式十分方便,但是有时候可能需要将PDF文件转换为Word文档。幸运的是,Python提供了多种方法来实现这个目标。 为什么要将PDF转换为Word&a…

6. 常见的文件编码方式及查看网页源码的编码方式

6. 常见的文件编码方式及查看网页源码的编码方式 文章目录 6. 常见的文件编码方式及查看网页源码的编码方式1. 编码的由来2. 编码的作用3. 常见的编码方式4. ASCII码5. Unicode符号集6. UTF-87. GB23128. GBK9. ISO-8859-110. 文件编码查看方式11. 网页编码查看方式12. 留言 1.…

【学习日记2023.6.9】之 SpringCloud入门

文章目录 SpringCloud1. 认识微服务1.1 单体架构1.2 分布式架构1.3 微服务1.4 SpringCloud1.5 总结 2. 服务拆分和远程调用2.1 服务拆分原则2.2 服务拆分示例2.2.1 导入Sql语句2.2.2 导入demo工程 2.3 实现远程调用案例2.3.1 案例需求:2.3.2 注册RestTemplate2.3.3 …