客户端实现
场景描述
本文介绍如何通过环信 IM SDK 和 Agora RTC SDK 在 Android 项目里实现语聊房的主要功能。
技术原理
房间管理
语聊房 Demo 内部依赖环信 IM 聊天室与 Agora RTC 频道。
以下为房间管理流程图。
Demo 中创建或加入语聊房的流程如下:
- 启动 Demo。在
application
中调用ChatroomConfigManager.getInstance.initRoomConfig
方法初始化环信 IM SDK。 - 利用手机号和验证码登录环信 IM。
- 登录 IM 后,通过 App Server 创建语聊房。成功后,房间创建者成为房主自动加入语聊房并跳转至语聊房页面。
- 进入房间后加入环信 IM 聊天室和 RTC 频道。 首先检查是否具有 RTC 所需权限。获取到所需权限并得到授权后执行
onPermissionGrant
方法,调用initSdkJoin
进行 RTC 初始化(初始化 RTC 加入频道需要的参数由登录时由 App Server 返回)。 - 检查是否成功加入环信 IM 聊天室 和 RTC 频道。
- 若成功加入,房主可以发送本地音频流和消息,观众可以发消息。
- 若未成功加入,则退出房间,返回房间列表页面。
- 退出房间。
语聊房的用户角色如下表所示:
角色 | 描述 |
---|---|
房主 | 语聊房创建者,在 Demo 中占用 0 号麦位,不可修改。房主可以接收、发送音频流和消息。 |
主播 | 进入语聊房后,通过上麦成为主播,可以接收、发送音频流和消息。 |
观众 | 进入语聊房后,未进行上麦的成员只能发送和接收消息。申请上麦或房主邀请上麦成功后,可以和主播实时互动。 |
语聊房的相关操作如下表所示:
操作 | 描述 |
---|---|
创建房间 | 房主调用 App Server 的 ChatroomHttpManager.getInstance.createRoom 方法创建语聊房,创建成功后自动加入语聊房。房主自动上麦成为主播,更新房间内麦位信息。创建语聊房前,房主调用 ChatroomConfigManager.getInstance.initRoomConfig 方法初始化环信 IM SDK。 |
加入房间 | 用户调用 App Server 的 ChatroomHttpManager.getInstance.joinRoom 方法加入语聊房。加入语聊房前,首先调用 ChatroomHttpManager.getInstance.loginWithToken 方法登录环信 IM,然后调用 initSdkJoin 进行 RTC 初始化。 |
离开房间 | 观众或主播可调用 ChatroomHttpManager.getInstance.leaveRoom 方法离开语聊房。房主离开语聊房,语聊房对象自动销毁,其他成员自动离开语聊房。 |
发送音频流 | 观众或主播可调用 RtcRoomController.get.joinChannel 方法发送音频流。 |
发送消息 | 语聊房内的用户可调用 ChatroomHelper.getInstance.sendTextMsg 或者 CustomMsgHelper.getInstance.sendCustomMsg 方法发送消息。观众也可以调用 ChatroomHelper.getInstance.sendGiftMsg 方法发送礼物消息。 |
麦位管理
麦位管理流程图如下所示:
麦位相关操作如下表所示:
操作 | 描述 |
---|---|
邀请上麦 | 房主调用 ChatroomHttpManager.getInstance.invitationMic 方法邀请观众上麦。观众收到 ChatroomListener.receiveInviteSite 回调,选择是否上麦。- 观众调用 ChatroomHttpManager.getInstance.agreeMicInvitation 方法同意上麦,成为主播,房间内所有用户会收到 ChatroomListener.roomAttributesDidUpdated ,更新房间内麦位信息。- 观众调用 ChatroomHttpManager.getInstance.rejectMicInvitation 方法拒绝上麦,房主收到 ChatroomListener receiveInviteRefusedSite 回调。 |
申请上麦 | 观众调用 ChatroomHttpManager.getInstance.submitMic 方法向房主申请上麦。房主收到 ChatroomListener.receiveApplySite 回调,选择同意或拒绝申请。- 房主调用 ChatroomHttpManager.getInstance.applySubmitMic 方法同意上麦申请,房间内所有用户会收到 ChatroomListener roomAttributesDidUpdated 回调,更新房间内麦位信息。- 房主调用 ChatroomHttpManager.getInstance.rejectSubmitMic 方法拒绝上麦申请,申请者收到 ChatroomListener receiveDeclineApply 回调。 |
撤销上麦申请 | 观众向房主申请上麦后,可以调用 ChatroomHttpManager.getInstance.cancelSubmitMic 方法撤销上麦申请。 |
下麦 | 下麦分为主动和被动两种方式: - 主动下麦:主播可调用 ChatroomHttpManager.getInstance.leaveMic 方法下麦成为观众。- 被踢下麦:房主调用 ChatroomHttpManager.getInstance.kickMic 方法对所在麦位主播发起下麦指令。对于这两种下麦方式,房间内所有用户都会收到 ChatroomListener.roomAttributesDidUpdated 回调,更新房间内麦位信息。 |
开麦 | 房主和主播调用 ChatroomHttpManager.getInstance.cancelCloseMic 方法开麦,发言时房间内其他用户可听到。房间内所有用户会收到 ChatroomListener roomAttributesDidUpdated 回调,更新麦位状态。 |
关麦 | 房主和主播调用 ChatroomHttpManager.getInstance.closeMic 方法关麦,关闭自己的声音。房间内所有用户会收到 ChatroomListener roomAttributesDidUpdated 回调,更新麦位状态。 |
禁麦 | 房主调用 ChatroomHttpManager.getInstance.muteMic 方法禁麦,不允许指定连麦主播发言,该主播的音频流将关闭。包括主播在内的房间内所有用户会收到 ChatroomListener roomAttributesDidUpdated 回调,更新麦位状态。 |
解禁麦位 | 房主调用 ChatroomHttpManager.getInstance.cancelMuteMic 方法解禁麦位,恢复连麦主播的发言权限,即恢复该麦位主播的音频流。包括主播在内的房间内所有用户会收到 ChatroomListener.roomAttributesDidUpdated 回调,更新麦位状态。 |
锁麦 | 房主调用 ChatroomHttpManager.getInstance.lockMic 方法锁麦,不允许任何人上该麦位。锁麦时,若该麦位有主播连麦,该主播收到 ChatroomListener.roomAttributesDidUpdated 回调被踢下来成为普通观众。房间内所有用户也会收到该回调,更新房间内麦位信息。 |
解锁麦位 | 房主调用 ChatroomHttpManager.getInstance.cancelLockMic 方法解锁麦位,使指定麦位恢复空闲状态,观众可申请该麦位上麦。房间内所有用户会收到 ChatroomListener roomAttributesDidUpdated 回调,更新麦位状态。 |
换麦 | 主播调用 ChatroomHttpManager.getInstance.exChangeMic 方法换麦,即从当前麦位切换到另一个空闲麦位。包括主播在内的房间内所有用户会收到 ChatroomListener roomAttributesDidUpdated 回调,更新房间内麦位信息。 |
发送单向消息
邀请用户上麦的流程如下:
- 房主调用 RESTFul API 邀请用户上麦。
- App Server 收到邀请消息后,利用房主角色向用户发送该消息。
- 用户同意邀请后调用 RESTful API 上麦。
- App Server 设置聊天室属性通知房间全员刷新房间设置。
用户申请上麦的流程如下:
- 用户调用 RESTful 接口申请上麦。
- App Server 收到上麦申请消息后以用户角色向房主发送该消息。
- 房主收到上麦申请后调用 RESTful API 同意上麦。
- App Server 设置聊天室属性通知房间全员刷新房间设置。
发送礼物消息
前提条件
- Android Studio 3.0 或以上版本;
- Android SDK API 等级 23 或以上;
- Android 6.0 或以上版本的设备;
- 有效的环信 IM 开发者账号和 App key,详见环信即时通讯云控制台;
- 如果你的网络环境部署了防火墙,请联系环信技术支持设置白名单。
项目配置
基础 API 参考
下表提供环信 IM SDK 和 Agora RTC SDK 的基本 API 参考。
初始设置
加入语聊房前,进行环信 IM SDK 初始化和 Agora RTC 初始化设置。
App Server 相关
以下表格为 App Server 的基础 API。
房间管理 API 如下表所示:
麦位管理 API 如下表所示:
礼物榜单 API 如下表所示:
API | 实现功能 |
---|---|
ChatroomHttpManager.getInstance.getGiftList | 获取赠送礼物榜单。 |
ChatroomHttpManager.getInstance.sendGift | 赠送礼物。 |
Agora RTC 相关
API | 实现功能 |
---|---|
initializeAndJoinChannel | 初始化 app 并加入频道。 |
其他 API,详见声网官网。
Easemob IM SDK 相关
Easemob IM SDK 的 API 如下表所示:
API | 实现功能 |
---|---|
EMClient.getInstance.init | 初始化 IM SDK。 |
EMClient.getInstance.chatManager.sendMessage | 发送消息。 |
语聊房监听
初始化 SDK(ChatroomConfigManager.getInstance.initRoomConfig)时已注册了连接监听、消息监听、聊天室监听,可以实现 ChatroomListener
接口。
音频相关
主播调用以下方法设置音频流:
API | 实现功能 |
---|---|
setAudioProfile [2/2] | 设置音频编码属性。 |
muteLocalAudioStream | 主播可以关闭或开启本地麦克风。 |
adjustRecordingSignalVolume | 调节人声音量。 |
附加功能
最佳音效
设置最佳音效,示例代码如下:
annotation class SoundSelection {
companion object {
const val SocialChat = 0 //社交语聊
const val Karaoke = 1 //在线 K 歌
const val GamingBuddy = 2 //游戏陪玩
const val SoundCardHQ = 3 //专业主播
}
}
rtcEngine?.apply {
when (config.soundType) {
SoundSelection.SocialChat, SoundSelection.Karaoke -> { // 社交语聊,KTV
setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
setAudioProfile(Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY)
setAudioScenario(Constants.AUDIO_SCENARIO_GAME_STREAMING)
}
SoundSelection.GamingBuddy -> { // 游戏陪玩
setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION)
}
else -> { //专业主播
setAudioProfile(Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY)
setAudioScenario(Constants.AUDIO_SCENARIO_GAME_STREAMING)
setParameters("{\"che.audio.custom_payload_type\":73}")
setParameters("{\"che.audio.custom_bitrate\":128000}")
// setRecordingDeviceVolume(128) 4.0.1 上才支持
setParameters("{\"che.audio.input_channels\":2}")
}
}
}
AI 降噪
AI 降噪插件使用声网人工智能噪声消除算法,能够让远程交流和面对面交谈一样实时。
可以开启或关闭 AI 降噪以及设置中级降噪和高级降噪。示例代码如下:
internal class AgoraRtcDeNoiseEngine : RtcBaseDeNoiseEngine<RtcEngineEx>() {
override fun closeDeNoise(): Boolean {
engine?.apply {
setParameters("{\"che.audio.ains_mode\":0}")
setParameters("{\"che.audio.nsng.lowerBound\":80}")
setParameters("{\"che.audio.nsng.lowerMask\":50}")
setParameters("{\"che.audio.nsng.statisticalbound\":5}")
setParameters("{\"che.audio.nsng.finallowermask\":30}")
setParameters("{\"che.audio.nsng.enhfactorstastical\":200}")
}
return true
}
override fun openMediumDeNoise(): Boolean {
engine?.apply {
setParameters("{\"che.audio.ains_mode\":2}")
setParameters("{\"che.audio.nsng.lowerBound\":80}")
setParameters("{\"che.audio.nsng.lowerMask\":50}")
setParameters("{\"che.audio.nsng.statisticalbound\":5}")
setParameters("{\"che.audio.nsng.finallowermask\":30}")
setParameters("{\"che.audio.nsng.enhfactorstastical\":200}")
}
return true
}
override fun openHeightDeNoise(): Boolean {
engine?.apply {
setParameters("{\"che.audio.ains_mode\":2}")
setParameters("{\"che.audio.nsng.lowerBound\":10}")
setParameters("{\"che.audio.nsng.lowerMask\":10}")
setParameters("{\"che.audio.nsng.statisticalbound\":0}")
setParameters("{\"che.audio.nsng.finallowermask\":8}")
setParameters("{\"che.audio.nsng.enhfactorstastical\":200}")
}
return true
}
}