在线咨询
专属客服在线解答,提供专业解决方案
工单支持
专业技术支持团队,随时响应服务需求

语聊房 iOS 开发教程:SDK 接入与 AudioSession 配置

iOS 接语聊房,有两件 Android 没有的必要配置:Background Modes 里要开启 Audio,App 切后台后音频才不会中断;还要注意 AVAudioSession 的时序问题。声网 SDK 进频道时自动配置 AVAudioSession,如果项目里有其他库(播放器、录音模块)也在操作它,轻则麦克风不出声,重则蓝牙耳机切换失效。

从 CocoaPods 接入(pod 'ShengwangRtcEngine_iOS')、Info.plist 权限声明、Background Modes 开启,到 SDK 初始化、Token 鉴权、上下麦切换,本文逐步说明,附 AVAudioSession 冲突处理和常见问题排查。


一. 添加 SDK 依赖

CocoaPods 接入

在 Podfile 里添加:

pod 'ShengwangRtcEngine_iOS'

执行:

pod install

安装完成后,打开 .xcworkspace 文件而不是 .xcodeproj,这是 CocoaPods 项目的标准打开方式。如果项目目录里还没有 Podfile,先在项目根目录执行 pod init 生成一个。

Swift Package Manager 接入

Xcode 14 以上可以用 SPM。在 Xcode 菜单里选 File → Add Package Dependencies,填入声网 iOS SDK 的 GitHub 仓库地址,选择需要的版本规则。SPM 不需要安装额外工具,适合不想引入 CocoaPods 的项目。

两种方式功能一样,选哪种主要看项目已有的依赖管理习惯。


二. 配置麦克风权限

iOS 要求在 Info.plist 里声明麦克风用途,没有这个条目,应用会在请求麦克风时被系统直接拒绝:

<key>NSMicrophoneUsageDescription</key>
<string>加入语聊房需要使用麦克风</string>

字符串内容是系统权限弹窗里显示给用户的说明,写清楚实际用途,App Store 审核时会检查这里的描述是否和功能对应。

语聊房是纯音频场景,不需要 NSCameraUsageDescription

运行时权限检查

系统会在第一次调用麦克风相关 API 时自动弹权限请求,但在进入频道前主动检查权限状态,可以更清晰地处理用户拒绝的情况:

import AVFoundation

func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
    switch AVAudioSession.sharedInstance().recordPermission {
    case .granted:
        completion(true)
    case .denied:
        // 用户已拒绝,只能引导去设置里手动开启
        completion(false)
    case .undetermined:
        AVAudioSession.sharedInstance().requestRecordPermission { granted in
            DispatchQueue.main.async {
                completion(granted)
            }
        }
    @unknown default:
        completion(false)
    }
}

用户拒绝权限后,引导他们去系统设置:

if let url = URL(string: UIApplication.openSettingsURLString) {
    UIApplication.shared.open(url)
}

三. 开启后台音频

语聊房用户切到其他 App 时,音频应该继续播放。iOS 默认会在 App 进入后台后暂停音频,需要开启 Background Modes:

  1. 在 Xcode 中打开项目的 Target
  2. 选择 Signing & Capabilities 标签页
  3. 点击左上角 + Capability,添加 Background Modes
  4. 勾选 Audio, AirPlay, and Picture in Picture

开启后,Info.plist 里会自动添加:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

不开启这个能力,用户按下 Home 键后几秒音频就会中断。语聊房场景里用户经常在听房的同时使用其他 App,这个配置是基础。


四. 初始化 SDK

SDK 入口是 AgoraRtcEngineKit,通过 sharedEngine(with:delegate:) 创建:

import AgoraRtcKit

class ChatRoomViewController: UIViewController {

    var agoraKit: AgoraRtcEngineKit!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupAgoraKit()
    }

    private func setupAgoraKit() {
        let config = AgoraRtcEngineConfig()
        // 从配置文件或环境变量读取,不要硬编码在代码里
        config.appId = AppConfig.agoraAppId

        agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)

        // 语聊房必须使用直播模式
        // 直播模式支持 broadcaster/audience 角色区分
        // 通话模式下所有用户默认都是发布者,不适合语聊房
        agoraKit.setChannelProfile(.liveBroadcasting)

        // 默认以观众身份加入,上麦时再切换
        agoraKit.setClientRole(.audience)

        // 语聊房不调用 enableVideo(),降低功耗,也不会触发摄像头权限请求
    }
}

setChannelProfile(.liveBroadcasting) 是语聊房区别于普通通话最关键的一步。通话模式下所有人都默认发布音频,语聊房需要明确区分哪些用户在麦位上(broadcaster)、哪些只是收听(audience)。

sharedEngine 是耗时操作,不要在主线程直接调用,放到后台线程或用 async/await 处理。


五. 加入频道

加入频道前需要从服务端获取 Token(详见:《语聊房 Token 怎么生成》)。通过 AgoraRtcChannelMediaOptions 指定进入频道时的初始状态:

var currentChannelName: String = ""
var currentUid: UInt = 0

func joinChannel(token: String, channelName: String, uid: UInt) {
    self.currentChannelName = channelName
    self.currentUid = uid

    let options = AgoraRtcChannelMediaOptions()
    options.clientRoleType = .audience      // 默认观众
    options.publishMicrophoneTrack = false  // 观众不发布音频
    options.autoSubscribeAudio = true       // 自动订阅频道内所有音频

    let result = agoraKit.joinChannel(
        byToken: token,
        channelId: channelName,
        uid: uid,
        mediaOptions: options
    )

    if result != 0 {
        print("joinChannel 失败,错误码: \(result)")
    }
}

autoSubscribeAudio = true 让观众自动收听频道内所有音频,不需要手动订阅每个发布者。publishMicrophoneTrack = false 确保以观众身份进入时不会意外发布音频。


六. 处理加入频道回调

实现 AgoraRtcEngineDelegate

extension ChatRoomViewController: AgoraRtcEngineDelegate {

    func rtcEngine(_ engine: AgoraRtcEngineKit,
                   didJoinChannel channel: String,
                   withUid uid: UInt,
                   elapsed: Int) {
        print("加入频道成功: \(channel), uid: \(uid), 耗时: \(elapsed)ms")
        DispatchQueue.main.async {
            // 更新 UI,显示麦位初始状态
        }
    }

    func rtcEngine(_ engine: AgoraRtcEngineKit,
                   didOfflineOfUid uid: UInt,
                   reason: AgoraUserOfflineReason) {
        // 其他用户离线(主动离开或断线超时)
        print("用户离线: uid=\(uid), reason=\(reason.rawValue)")
        DispatchQueue.main.async {
            // 清除对应麦位 UI
        }
    }

    func rtcEngine(_ engine: AgoraRtcEngineKit,
                   didLeaveChannelWith stats: AgoraChannelStats) {
        print("已离开频道, 通话时长: \(stats.duration)s")
    }
}

所有 UI 更新都要切回主线程执行,SDK 回调在内部线程触发。


七. 上麦与下麦

iOS SDK 通过 updateChannel(with:) 切换角色,切换立即生效,不需要重新加入频道:

// 上麦:切换为 broadcaster,开始发布麦克风音频
func goOnMic() {
    let options = AgoraRtcChannelMediaOptions()
    options.clientRoleType = .broadcaster
    options.publishMicrophoneTrack = true
    agoraKit.updateChannel(with: options)
}

// 下麦:切回 audience,停止发布
func goOffMic() {
    let options = AgoraRtcChannelMediaOptions()
    options.clientRoleType = .audience
    options.publishMicrophoneTrack = false
    agoraKit.updateChannel(with: options)
}

切换结果通过 didClientRoleChanged 回调确认:

func rtcEngine(_ engine: AgoraRtcEngineKit,
               didClientRoleChanged oldRole: AgoraClientRole,
               newRole: AgoraClientRole,
               newRoleOptions: AgoraClientRoleOptions?) {
    DispatchQueue.main.async {
        switch newRole {
        case .broadcaster:
            // 上麦成功,激活麦位 UI,显示发言状态
            break
        case .audience:
            // 下麦成功,恢复麦位为空位 UI
            break
        @unknown default:
            break
        }
    }
}

已在麦位上的 broadcaster 可以临时静音,不需要切换角色:

// 静音(仍是 broadcaster,只是暂停发送音频)
agoraKit.muteLocalAudioStream(true)

// 取消静音
agoraKit.muteLocalAudioStream(false)

房主强制静音他人,SDK 层面没有直接 API,需要通过信令通知目标用户的客户端执行 muteLocalAudioStream(true)。这个交互逻辑需要在信令层设计,不是 RTC 的责任范围。


八. Token 过期处理

extension ChatRoomViewController: AgoraRtcEngineDelegate {

    // Token 即将过期(过期前 30 秒触发)
    func rtcEngine(_ engine: AgoraRtcEngineKit,
                   tokenPrivilegeWillExpire token: String) {
        fetchTokenFromServer(channelName: currentChannelName, uid: currentUid) { [weak self] newToken in
            self?.agoraKit.renewToken(newToken)
            // renewToken 成功后用户不会掉线,无感续期
        }
    }

    // Token 已过期(tokenPrivilegeWillExpire 没有及时处理时触发)
    func rtcEngineRequestToken(_ engine: AgoraRtcEngineKit) {
        fetchTokenFromServer(channelName: currentChannelName, uid: currentUid) { [weak self] newToken in
            guard let self = self else { return }
            let options = AgoraRtcChannelMediaOptions()
            options.clientRoleType = .audience
            self.agoraKit.joinChannel(
                byToken: newToken,
                channelId: self.currentChannelName,
                uid: self.currentUid,
                mediaOptions: options
            )
        }
    }
}

两个回调都要实现。tokenPrivilegeWillExpire 是提前换,用 renewToken 续期,用户无感知;rtcEngineRequestToken 是 Token 已经过期的兜底,需要重新加入频道。网络差的情况下 tokenPrivilegeWillExpire 可能没来得及处理,rtcEngineRequestToken 确保用户最终能恢复连接。


九. 离开频道和资源释放

func leaveChannel() {
    agoraKit.leaveChannel(nil)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    leaveChannel()
}

deinit {
    // destroy() 释放所有 SDK 资源,下次需要重新 sharedEngine(with:delegate:)
    AgoraRtcEngineKit.destroy()
}

leaveChanneldestroy 分开调用。如果 ViewController 销毁时没有主动调用 leaveChannel,SDK 会在 destroy 时自动处理,但主动调用能确保回调正常触发,便于做离开频道后的 UI 清理。


十. AVAudioSession 注意事项

声网 SDK 在加入频道时会自动配置 AVAudioSession,设置合适的 category 和 options 来支持录音、播放和蓝牙耳机切换。多数情况下不需要手动配置。

有其他代码操作 AVAudioSession 时需要注意时序:

  • 在调用 joinChannel 之前不要修改 AVAudioSession.category,SDK 加入频道时会覆盖配置
  • 退出频道后,SDK 会将 AVAudioSession 恢复到接入前的状态
  • 如果某个第三方库在 SDK 工作期间修改了 AVAudioSession,可能导致 SDK 行为异常,需要排查是哪一方修改了 session

处理电话打断

系统电话进来时,AVAudioSession 会被中断,SDK 会暂停音频。通话结束后多数情况 SDK 会自动恢复,但某些机型需要手动处理:

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleAudioInterruption),
        name: AVAudioSession.interruptionNotification,
        object: nil
    )
}

@objc func handleAudioInterruption(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }

    if type == .ended {
        let options = (userInfo[AVAudioSessionInterruptionOptionKey] as? UInt)
            .flatMap { AVAudioSession.InterruptionOptions(rawValue: $0) }
        if options?.contains(.shouldResume) == true {
            try? AVAudioSession.sharedInstance().setActive(true)
        }
    }
}

十一. 音频质量配置

setChannelProfile 之后添加:

// 标准语音质量,普通语聊场景(默认值)
agoraKit.setAudioProfile(.default)

// 高质量音乐,KTV / 合唱场景
agoraKit.setAudioProfile(.musicHighQuality)

普通社交语聊用默认配置足够。KTV 场景对音质保真度和延迟有更高要求,用 .musicHighQuality,同时需要配合耳返功能。


十二. 常见问题

模拟器里没有音频

iOS 模拟器不支持麦克风,joinChannel 可以成功,但无法发布音频。测试麦克风相关功能必须用真机。

进频道后收不到其他人的声音

先确认发送方已设为 .broadcasterpublishMicrophoneTrack = true。通过 reportAudioVolumeIndication 可以实时监控频道内各用户的音量,用来区分是发送端还是接收端的问题:

// 每 200ms 上报一次音量数据
agoraKit.enableAudioVolumeIndication(200, smooth: 3, reportVad: false)
func rtcEngine(_ engine: AgoraRtcEngineKit,
               reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo],
               totalVolume: Int) {
    for speaker in speakers {
        print("uid: \(speaker.uid), volume: \(speaker.volume)")
    }
}

uid 为 0 代表本地用户。如果本地有音量但远端收不到,问题在网络或发布配置;如果本地也没有音量,先检查麦克风权限和 AVAudioSession 状态。

蓝牙耳机连接后没有声音

iOS 蓝牙音频需要 AVAudioSession 开启 .allowBluetooth 选项。SDK 默认会配置,如果遇到问题,检查是否有其他代码在 SDK 运行期间覆盖了 AVAudioSession 的 options,把 .allowBluetooth 去掉了。

进入后台后音频停了

确认 Xcode 里 Background Modes 已勾选 Audio 选项,并且 Info.plistUIBackgroundModes 包含 audio 字符串。如果是 Xcode 自动生成的旧项目,有时候要手动检查 Info.plist 里的值是否实际写入。

特定机型有明显回声

SDK 的 AEC(回声消除)自动处理大多数情况。如果特定机型外放时仍有回声,通常是扬声器和麦克风之间距离近导致 AEC 消不干净,让用户使用耳机是最直接的解决方式。也可以检查是否有其他 App 占用了麦克风,导致 SDK 拿到的音频已经被处理过。

 

在声网,连接无限可能

想进一步了解「对话式 AI 与 实时互动」?欢迎注册,开启探索之旅。

本博客为技术交流与平台行业信息分享平台,内容仅供交流参考,文章内容不代表本公司立场和观点,亦不构成任何出版或销售行为。