iOS端实现语聊房

本文档将介绍如何在您的 iOS 项目中集成 ARTC SDK, 快速实现一个简单的纯音频互动App,适用于语音通话、语聊房等场景。

功能介绍

在开始前,您需要了解以下有关音视频实时互动的基本概念:

  • ARTC SDK:阿里云实时音视频产品,帮助开发中快速实现实时音视频互动的SDK。

  • 频道:房间的概念,在同一个频道内的用户可以进行实时互动。

  • 主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。

  • 观众:可在频道内订阅音视频流,不能发布音视频流。

下图展示了实现音频通话及语聊房的基本流程:

image
  1. 用户需要先调用joinChannel加入频道,才能进行推流、拉流:

    • 普通纯音频通话场景:所有用户都是主播角色,可以进行推流和拉流;

    • 语聊房场景:需要在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色;

    • 通过setClientRole为用户设置不同的角色。

  2. 加入频道后,不同角色的用户有不同的推拉流行为:

    • 所有频道内的用户都可以接收相同频道内的音视频流;

    • 主播角色可以在频道内推音视频流;

    • 观众如果需要推流,需要调用setClientRole方法,将用户角色切换成主播,便可以推流。

示例项目

阿里云ARTC SDK提供了开源的示例项目供客户参考,您可以前往下载或查看示例源码

前提条件

在实现功能以前,请确保您的开发环境满足:

  • 开发工具:Xcode 14.0 及以上版本,推荐使用最新正式版本。

  • 配置推荐:CocoaPods 1.9.3 及以上版本。

  • 测试设备:iOS 9.0 及以上版本的测试设备。

说明

推荐使用真机测试,模拟机可能存在功能缺失。

  • 网络环境:需要稳定的网络连接。

  • 应用准备:获取实时音视频应用的AppIDAppKey,详情请参见创建应用

  • 创建项目和配置:已创建项目并为项目添加了音频、网络等音视频互动的相关权限,此外需要集成 ARTC SDK,相关步骤请参考实现音视频通话

实现步骤

下面将以语聊房场景为例进行演示,相关功能时序如下:

image

语聊房场景主要特点如下:

  • 纯音频:频道内仅包含音频,不包含视频。

  • 主播/观众角色:频道内角色分为主播和观众角色,主播角色可以推拉音频流,观众角色只能拉取主播推送的音频流;观众角色可以切换为主播角色。

实现纯音频互动

1、申请权限请求

进入音视频通话时,虽然SDK会检查是否已在App中授予了所需要的权限,当为保障体验,建议在发起通话前检查视频拍摄及麦克风采集的权限。

func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
    let status = AVCaptureDevice.authorizationStatus(for: .audio)
    
    switch status {
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .audio) { granted in
            completion(granted)
        }
    case .authorized:
        completion(true)
    default:
        completion(false)
    }
}

func checkCameraPermission(completion: @escaping (Bool) -> Void) {
    let status = AVCaptureDevice.authorizationStatus(for: .video)
    
    switch status {
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .video) { granted in
            completion(granted)
        }
    case .authorized:
        completion(true)
    default:
        completion(false)
    }
}

// 使用示例
checkMicrophonePermission { granted in
    if granted {
        print("用户已授权麦克风")
    } else {
        print("用户未授权麦克风")
    }
}

checkCameraPermission { granted in
    if granted {
        print("用户已授权摄像头")
    } else {
        print("用户未授权摄像头")
    }
}

2、鉴权Token

加入ARTC频道需要一个鉴权Token,用于鉴权用户的合法身份,其鉴权Token生成规则参见:Token鉴权。Token 生成有两种方式:单参数方式和多参数方式,不同的Token生成方式需要调用SDK不同的加入频道(joinChannel)的接口。

上线发布阶段

由于Token的生成需要使用AppKey,写死在客户端存在泄漏的风险,因此强烈建议线上业务通过业务Server生成下发给客户端。

开发调试阶段

开发调试阶段,如果业务Server还没有生成Token的逻辑,可以暂时参考APIExample上的Token生成逻辑,生成临时Token,其参考代码如下:

class ARTCTokenHelper: NSObject {

    /**
    * RTC AppId
    */
    public static let AppId = "<RTC AppId>"

    /**
    * RTC AppKey
    */
    public static let AppKey = "<RTC AppKey>"

    /**
    * 根据channelId,userId, timestamp 生成多参数入会的 token
    * Generate a multi-parameter meeting token based on channelId, userId, and timestamp
    */
    public func generateAuthInfoToken(appId: String = ARTCTokenHelper.AppId, appKey: String =  ARTCTokenHelper.AppKey, channelId: String, userId: String, timestamp: Int64) -> String {
        let stringBuilder = appId + appKey + channelId + userId + "\(timestamp)"
        let token = ARTCTokenHelper.GetSHA256(stringBuilder)
        return token
    }

    /**
    * 根据channelId,userId, nonce 生成单参数入会 的token
    * Generate a single-parameter meeting token based on channelId, userId, and nonce
    */
    public func generateJoinToken(appId: String = ARTCTokenHelper.AppId, appKey: String =  ARTCTokenHelper.AppKey, channelId: String, userId: String, timestamp: Int64, nonce: String = "") -> String {
        let token = self.generateAuthInfoToken(appId: appId, appKey: appKey, channelId: channelId, userId: userId, timestamp: timestamp)

        let tokenJson: [String: Any] = [
            "appid": appId,
            "channelid": channelId,
            "userid": userId,
            "nonce": nonce,
            "timestamp": timestamp,
            "token": token
        ]

        if let jsonData = try? JSONSerialization.data(withJSONObject: tokenJson, options: []),
        let base64Token = jsonData.base64EncodedString() as String? {
            return base64Token
        }

        return ""
    }

    /**
    * 字符串签名
    * String signing (SHA256)
    */
    private static func GetSHA256(_ input: String) -> String {
        // 将输入字符串转换为数据
        let data = Data(input.utf8)

        // 创建用于存储哈希结果的缓冲区
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))

        // 计算 SHA-256 哈希值
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
        }

        // 将哈希值转换为十六进制字符串
        return hash.map { String(format: "%02hhx", $0) }.joined()
    }

}

3. 创建并初始化引擎

  • 创建 RTC 引擎

调用getInstance创建 RTC 引擎对象。

private var rtcEngine: AliRtcEngine? = nil

// 创建引擎并设置回调
let engine = AliRtcEngine.sharedInstance(self, extras:nil)
self.rtcEngine = engine
  • 初始化引擎

    • 调用setChannelProfile接口设置频道为互动模式。

    • 根据业务场景中用户的角色,调用setClientRole接口为用户设置主播/观众角色。

    • 调用setAudioProfile接口设置音频质量与场景模式。

// 设置频道模式为互动模式,RTC下都使用AliRtcInteractivelive
engine.setChannelProfile(AliRtcChannelProfile.interactivelive)
// 设置角色
if self.isAnchor {
    // 主播模式,需要推音视频流,设置AliRtcClientRoleInteractive
    engine.setClientRole(AliRtcClientRole.roleInteractive)
}
else {
    // 观众模式,不需要推音视频流,设置AliRtcClientRolelive
    engine.setClientRole(AliRtcClientRole.rolelive)
}

// 设置音频Profile,默认使用高音质模式AliRtcEngineHighQualityMode及音乐模式AliRtcSceneMusicMode
engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)
  • 实现常用回调

SDK 在运行过程中如遇到异常情况,会优先尝试内部重试机制以自动恢复。对于无法自行解决的错误,SDK 会通过预定义的回调接口通知您的应用程序。

以下是一些 SDK 无法处理、需由应用层监听和响应的关键回调:

异常发生原因

回调及参数

解决方案

说明

鉴权失败

onJoinChannelResult回调中的result返回AliRtcErrJoinBadToken

发生错误时App需要检查Token是否正确。

在用户主动调用API时,若鉴权失败,系统将在调用API的回调中返回鉴权失败的错误信息。

鉴权将要过期

onWillAuthInfoExpire

发生该异常时App需要重新获取最新的鉴权信息后,再调用refreshAuthInfo刷新鉴权信息。

鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。

鉴权过期

onAuthInfoExpired

发生该异常时App需要重新入会。

鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。

网络连接异常

onConnectionStatusChange回调返回AliRtcConnectionStatusFailed

发生该异常时APP需要重新入会。

SDK具备一定时间断网自动恢复能力,但若断线时间超出预设阈值,会触发超时并断开连接。此时,App应检查网络状态并指导用户重新加入会议。

被踢下线

onBye

  • AliRtcOnByeUserReplaced:当发生该异常时排查用户userid是否相同。

  • AliRtcOnByeBeKickedOut:当发生该异常时,表示被业务踢下线,需要重新入会。

  • AliRtcOnByeChannelTerminated:当发生该异常时,表示房间被销毁,需要重新入会。

RTC服务提供了管理员可以主动移除参与者的功能。

本地设备异常

onLocalDeviceException

发生该异常时App需要检测权限、设备硬件是否正常。

RTC服务支持设备检测和异常诊断的能力;当本地设备发生异常时,RTC服务会通过回调的方式通知客户本地设备异常,此时,若SDK无法自行解决问题,则App需要介入以查看设备是否正常。

extension VideoCallMainVC: AliRtcEngineDelegate {

    func onJoinChannelResult(_ result: Int32, channel: String, elapsed: Int32) {
        "onJoinChannelResult1 result: \(result)".printLog()
    }

    func onJoinChannelResult(_ result: Int32, channel: String, userId: String, elapsed: Int32) {
        "onJoinChannelResult2 result: \(result)".printLog()
    }

    func onRemoteUser(onLineNotify uid: String, elapsed: Int32) {
        // 远端用户的上线
        "onRemoteUserOlineNotify uid: \(uid)".printLog()
    }

    func onRemoteUserOffLineNotify(_ uid: String, offlineReason reason: AliRtcUserOfflineReason) {
        // 远端用户的下线
        "onRemoteUserOffLineNotify uid: \(uid) reason: \(reason)".printLog()
    }


    func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
        "onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack)  videoTrack: \(videoTrack)".printLog()
    }

    func onAuthInfoWillExpire() {
        "onAuthInfoWillExpire".printLog()

        /* TODO: 务必处理;Token即将过期,需要业务触发重新获取当前channel,user的鉴权信息,然后设置refreshAuthInfo即可 */
    }

    func onAuthInfoExpired() {
        "onAuthInfoExpired".printLog()

        /* TODO: 务必处理;提示Token失效,并执行离会与释放引擎 */
    }

    func onBye(_ code: Int32) {
        "onBye code: \(code)".printLog()

        /* TODO: 务必处理;业务可能会触发同一个UserID的不同设备抢占的情况 */
    }

    func onLocalDeviceException(_ deviceType: AliRtcLocalDeviceType, exceptionType: AliRtcLocalDeviceExceptionType, message msg: String?) {
        "onLocalDeviceException deviceType: \(deviceType)  exceptionType: \(exceptionType)".printLog()

        /* TODO: 务必处理;建议业务提示设备错误,此时SDK内部已经尝试了各种恢复策略已经无法继续使用时才会上报 */
    }

    func onConnectionStatusChange(_ status: AliRtcConnectionStatus, reason: AliRtcConnectionStatusChangeReason) {
        "onConnectionStatusChange status: \(status)  reason: \(reason)".printLog()

        if status == .failed {
            /* TODO: 务必处理;建议业务提示用户,此时SDK内部已经尝试了各种恢复策略已经无法继续使用时才会上报 */
        }
        else {
            /* TODO: 可选处理;增加业务代码,一般用于数据统计、UI变化 */
        }
    }
}

4. 设置推拉流属性

SDK 默认情况下会自动推送和拉取频道内的音视频流

  • 设置为观众模式后只能拉流,publishLocalAudioStream 无效

  • 对于主播和观众均可以设置为下面的配置

// 设置音频Profile,默认使用高音质模式AliRtcEngineHighQualityMode及音乐模式AliRtcSceneMusicMode
engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)

// 语聊场景,不需要publish视频
engine.publishLocalVideoStream(false)

// 设置默认订阅远端的音频
engine.setDefaultSubscribeAllRemoteAudioStreams(true)
engine.subscribeAllRemoteAudioStreams(true)

5. 加入频道开始纯音频互动

调用joinChannel接口加入频道。

注意:

如果token是单参数规则生成的,需要调用SDK单参数的joinChannel[1/3]接口,如果是多参数规则生成的,需要调用SDK多参数的joinChannel[2/3]接口。调用完加入频道后,可以在onJoinChannelResult回调中拿到加入频道结果,如果result0,则表示加入频道成功,否则需要检查传进来的Token是否非法。

self.rtcEngine?.joinChannel(joinToken, channelId: nil, userId: nil, name: nil) 

6. 结束纯音频互动

音频互动结束,需要离开房间并销毁引擎,按照下列步骤结束音视频互动

  1. 调用leaveChannel离会。

  2. 调用destroy销毁引擎,并释放相关资源。

self.rtcEngine?.leaveChannel()
AliRtcEngine.destroy()
self.rtcEngine = nil

7. (可选)观众上下麦

业务场景中,如果观众角色的用户想要推流,需要调用setClientRole将观众角色切换为主播角色。

// 切换为主播角色
self.rtcEngine?.setClientRole(AliRtcClientRole.roleInteractive)

// 切换为观众角色
self.rtcEngine?.setClientRole(AliRtcClientRole.rolelive)

相关文档