基于声网 Agora RTM + RTC SDK 实现 iOS 语音聊天室 —— 常见问题汇总 & 解决方案手册

目录

  1. 【无声/单边无声】------ 占问题的 60%+
  2. 【Token 相关报错】------ 进房失败、中途断连
  3. 【RTC 错误码速查】------ -102 / -121 / -17 / -7 / -3
  4. 【RTM 登录/发消息报错】------ NOT_LOGIN / TOKEN_EXPIRED / TIMEOUT / REJECTED
  5. 【RTM 断线重连状态机】------ 你到底该不该手动 re-login
  6. 【麦克风权限 & iOS 隐私弹窗】------ 真机静默失败的高危坑
  7. 【音频路由错乱】------ 插耳机/连蓝牙后声音跑到听筒
  8. 【后台/锁屏后没声音】------ iOS 系统限制
  9. 【iOS 14 本地网络弹窗】------ RTM SDK 经典惊喜
  10. 【内存/生命周期】------ sharedEngine重复初始化、没 leaveChannel 就 rejoin
  11. 【调试利器】------ 开日志 + Agora Analytics

一、无声 / 单边无声(最常见)

症状分类

表现 典型指向
本地能说话,远端听不到 本地采集没起来(权限/路由/mute)
远端能说话,本地听不到 远端没发流 或 本地没 sub(role/options 配错)
互相都听不到 channel 不一致 / Token 不对 / App ID 不匹配
刚进房有声音,几秒后没了 Token 过期 / 被 mute / AVAudioSession 被别的模块改了

排查清单(按顺序做)

✅ Step 1:先确认两人真的在同一个 channel

swift 复制代码
swift
swift
// 你收到的回调
func rtcEngine(_ engine: AgoraRtcEngineKit,
               didJoinChannel channel: String,
               withUid uid: UInt,
               elapsed: Int) {
    print("✅ joined channel = (channel), uid = (uid)")
}

很多"无声"本质是:两个人进了不同 channel(前后带空格、大小写不一致、拼接参数写错)。channel name 必须完全一致。


✅ Step 2:确认没被 mute / 音量设 0

语聊房最常见的"手滑式无声":

scss 复制代码
swift
swift
// ⚠️ 这些调用都会直接导致没声音
engine.muteLocalAudioStream(true)           // 本地不发流
engine.muteAllRemoteAudioStreams(true)       // 不听任何人
engine.adjustRecordingSignalVolume(0)       // 采集音量 0
engine.adjustPlaybackSignalVolume(0)        // 播放音量 0

自检代码(调试时打出来):

bash 复制代码
swift
swift
print("recording vol =", engine.recordingSignalVolume())
print("playback vol  =", engine.playbackSignalVolume())

✅ Step 3:确认 AgoraRtcChannelMediaOptions配对了

这是 4.x 最容易配错的一步

ini 复制代码
swift
swift
let opts = AgoraRtcChannelMediaOptions()

// 主播(上麦)
opts.clientRoleType         = .broadcaster
opts.publishMicrophoneTrack  = true    // ← 必须是 true,否则远端听不到你
opts.publishCameraTrack      = false
opts.autoSubscribeAudio      = true

// 观众(听别人)
opts.clientRoleType         = .audience
opts.publishMicrophoneTrack = false
opts.autoSubscribeAudio     = true     // ← 必须是 true,否则你听不到别人
opts.autoSubscribeVideo     = false

如果你用老 API(joinChannelByToken不带 mediaOptions),或者 publishMicrophoneTrack忘了设,didJoinedOfUid会触发但音频 tracks 没发布​ → 表现为"能看到人进来但没声音"。


✅ Step 4:Info.plist麦克风权限(真机必查)

vbnet 复制代码
xml
xml
<key>NSMicrophoneUsageDescription</key>
<string>语音聊天室需要访问麦克风</string>

iOS 10+ 如果这行缺失,系统会 直接拒绝授权且不弹窗,回调立刻返回 denied,表现为"进房成功但始终无声"。

另外在代码中主动检查:

swift 复制代码
swift
swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    AVAudioSession.sharedInstance().requestRecordPermission { granted in
        if !granted {
            // 弹引导去设置的 alert
        }
    }
}

✅ Step 5:检查是否被其他 App 抢占麦克风

官方枚举明确列出了 iOS 特有错误:

错误 含义 解法
AgoraAudioLocalErrorDeviceNoPermission (2) 没麦克风权限 引导去 设置→隐私→麦克风
AgoraAudioLocalErrorDeviceBusy (3) 麦克风被其他 App 占用(微信通话/Siri/录音中等) 提示用户关闭其它录音 App;空闲约 5s 后自动恢复,或 rejoin
AgoraAudioLocalErrorInterrupted (8) 被来电 / Siri / 闹钟中断 中止干扰源后可恢复

你还可以通过回调监听:

swift 复制代码
swift
swift
func rtcEngine(_ engine: AgoraRtcEngineKit,
               localAudioStateChanged state: AgoraAudioLocalState,
               error: AgoraAudioLocalError) {
    print("audio state=(state.rawValue), error=(error.rawValue)")
}

二、Token 相关报错(进不去 / 中途掉了)

典型报错码

错误码 常在哪里看到 含义
AgoraErrorCodeInvalidAppId (101/-3) join 失败 App ID 填错 / 项目没启用对应服务
AgoraErrorCodeInvalidToken (110) join 返回非 0 Token 跟 App ID / channel / uid 不匹配
AgoraErrorCodeTokenExpired (109) 中途突然掉 Token 生存期到了

✅ 正确姿势:监听两个回调 + renew

swift 复制代码
swift
swift
// 1) Token 即将过期 ------ 提前 30s 通知你换新
func rtcEngine(_ engine: AgoraRtcEngineKit,
               tokenPrivilegeWillExpire token: String) {
    print("⚠️ Token will expire, renewing...")
    fetchNewTokenFromServer { newToken in
        engine.renewToken(newToken)
    }
}

// 2) Token 已过期(极端:网络抖动导致来不及 renew)
func rtcEngineRequestToken(_ engine: AgoraRtcEngineKit) {
    print("❌ Token expired, rejoin required")
    fetchNewTokenFromServer { newToken in
        engine.leaveChannel(nil)
        // 重新 joinChannel(byToken:newToken:...)
    }
}

官方建议两种更新路径:

  • 优先 :调 renewToken(_:)(不断连热更新)
  • 兜底leaveChannel→ 拿新 Token → joinChannel重新加入

⚠️ 常见错误:Token 算错了 uid 不匹配 (服务端你把 uid 当 "123",SDK 里传 123/0混用)→ 表现为 INVALID_TOKEN


三、RTC 经典错误码速查

错误码 符号 触发场景 怎么修
-102 AgoraErrorCodeJoinChannelRejected / invalid channel name channelId 含非法字符 / nil / 超长 限制 channelId 正则 [a-zA-Z0-9_-]{1,64}
-121 invalid uid uid=0 混用没问题,但你如果手动指定 uid,不能是 0 或负数 用 0(SDK 分配)或服务端分配 1~UINT32_MAX-1
-17 AgoraErrorCodeJoinChannelRejected 已经在频道里又调了一次 joinChannel leaveChannel或判断 connectionChangedToState:reason:的状态
-7 not initialized sharedEngine还没走完就用它 保证 setupEngine 在 join 之前
-8 invalid state 典型:调 startEchoTest后没 stopEchoTest就 join 清理测试流程

四、RTM 登录/发消息报错速查

错误码 含义 修法
-10001 NOT_INITIALIZED SDK 没 init 就调 API AgoraRtmClientKit(config:)
-10002 NOT_LOGIN 没 login 就发消息/进频道 等 login 成功回调再 joinChannel
-10003 INVALID_APP_ID App ID 错 或 没开通 RTM 服务 控制台确认项目启用 RTM
-10005 INVALID_TOKEN Token 格式错 / 跟 userId 不匹配 确认 RTM Token 的服务端生成参数
-10009 TOKEN_EXPIRED RTM Token 过期 换新 RTM Token → loginByToken:
-10011 LOGIN_TIMEOUT 12s 内没连上 查网络 / 代理 / 防火墙白名单
-10012 LOGIN_REJECTED userId 被封禁 / App ID 没开 RTM 控制台查封禁
-10013 LOGIN_ABORTED 同 userId 在其他端登录挤掉 做"互踢"策略或允许多端共存(不同 uid)

五、RTM 断线重连 ------ 你到底要不要手动 re-login?

官方状态机逻辑(重要)

RTM 2.x 的连接状态迁移:

arduino 复制代码
纯文本
纯文本
IDLE
 ↓ login
CONNECTING → CONNECTED  ✅
       ↑
    断网 4s+
       ↓
  RECONNECTING  ← SDK 自动重试(你别管)
       ↓
  ┌─ 30s 内恢复 → CONNECTED(在线状态不变)
  └─ 超过 30s 仍未恢复 → 你被从在线列表移除 → 之后 recovery 成功也要重新 sync 状态
       ↓ 极端:2min 都无法恢复
   FAILED(不会自动重试,你要手动 login)

✅ 正确写法

swift 复制代码
swift
swift
func rtmClient(_ client: AgoraRtmClientKit,
               didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {

    let cur  = event.currentState
    let code = event.reasonCode

    switch cur {
    case .connected:
        print("✅ RTM connected")
        // 重新 join 消息频道(如果需要)
    case .reconnecting:
        print("🔄 RTM reconnecting, reason=(code)")
    case .disconnected:
        print("⚠️ RTM disconnected")
    case .failed:
        // ⚠️ SDK 不会自动重连了
        print("❌ RTM FAILED reason=(code)")
        // 你的业务决定是否 retry login
        // 但要防雪崩:加退避(exponential backoff)
    default: break
    }
}

关键原则:RECONNECTING阶段不要主动 logout+login ,让 SDK 自己跑;只有到 FAILED才介入。


六、麦克风权限 & 真机"静默失败"坑

这个坑的特征:模拟器好像能跑、真机进去不弹授权框、也不崩、就是没声音

根因:iOS 10+ 强制检查 Info.plistNSMicrophoneUsageDescription

完整合规检查清单

xml 复制代码
xml
xml
<!-- Info.plist -->
<key>NSMicrophoneUsageDescription</key>
<string>语音聊天室需要通过麦克风进行实时语音交流</string>

并在首次进房前触发一次:

objectivec 复制代码
swift
swift
AVAudioSession.sharedInstance().requestRecordPermission { ok in
    DispatchQueue.main.async {
        if ok { self.joinChannel() }
        else { /* 弹设置引导 */ }
    }
}

Apple 审核层面要注意:

  • 描述字符串不能空着也不能写废话("用于App功能"会被拒)
  • 多语言包要对应上

七、音频路由错乱(插耳机/连蓝牙后声音跑听筒)

语聊房默认路由策略:

SDK 场景 默认路由
Audio SDK + 通信场景 听筒(earpiece) ​ ← 很多人以为这是 bug
Live Broadcasting 扬声器(speaker)

✅ 语音聊天室推荐配置

arduino 复制代码
swift
swift
// 1) 进房前:设默认走扬声器(绝大多数语聊房期望的行为)
engine.setDefaultAudioRouteToSpeakerphone(true)

// 2) 进房后:动态切换
engine.setEnableSpeakerphone(true)   // 扬声器
engine.setEnableSpeakerphone(false)  // 回落听筒(少见)

⚠️ setDefaultAudioRouteToSpeakerphone必须在 join 之前 调,setEnableSpeakerphonejoin 之后调,调反了不生效。

如果用户的蓝牙耳机优先级更高,iOS 的音频路由优先级是:用户物理行为(插拔耳机/蓝牙)> 你的 setEnableSpeakerphone。这是系统设计,不要跟它对抗------能做的是监听路由变化后同步 UI 状态。


八、后台/锁屏后没声音(iOS 系统限制)

iOS 12.4+ 系统限制:App 切后台,系统自动停采集

✅ 要在 Xcode 里加 Background Modes:

css 复制代码
纯文本
纯文本
Signing & Capabilities → + Capability → Background Modes
☑️ Audio, AirPlay, and Picture in Picture
☑️ Background processing

同时确保:

  • 用户在前台已 join 成功
  • 没调过 disableAudio()/ disableLocalAudio()
  • SDK 的 localAudioStateChanged回调报告过 AgoraAudioLocalStateRecording

语聊房的现实取舍:加了 Audio Background Mode 后 App 可以在后台维持音频采集,但 Apple 审核可能追问你的后台必要性(如果只是"听"不需要采集,观众角色可以不申请)。


九、iOS 14 的"查找本地网络设备"弹窗

RTM SDK 早期版本在 iOS 14 触发本地网络权限弹窗。

解决方案(二选一)

  1. 升 RTM SDK ≥ 1.4.1(官方修了,弹窗不再出现,服务不受影响)
  2. 或在 Info.plist加描述占位(不推荐但可应急)
vbnet 复制代码
xml
xml
<key>NSLocalNetworkUsageDescription</key>
<string>语音聊天室需要本地网络以连接服务</string>

十、生命周期坑:重复 init / 没 leaveChannel 就 rejoin

❌ 典型翻车代码

scss 复制代码
swift
swift
// 用户快速点两次"进入房间"
func onTapEnter() {
    setupEngine()        // 又创了一次 sharedEngine
    joinChannel(...)     // 上一次还在频道里 → -17 或被覆盖
}

✅ 正确模式

swift 复制代码
swift
swift
final class VoiceRTCService {

    private(set) var engine: AgoraRtcEngineKit!
    private var hasJoined = false

    func ensureEngine(appId: String) {
        if engine == nil {
            let cfg = AgoraRtcEngineConfig()
            cfg.appId = appId
            cfg.channelProfile = .liveBroadcasting
            engine = .sharedEngine(with: cfg, delegate: self)
            engine.disableVideo()
            engine.enableAudio()
        }
    }

    func joinChannel(...) {
        guard !hasJoined else {
            print("⚠️ already in channel, skip or leave first")
            return
        }
        hasJoined = true
        engine.joinChannel(...)
    }

    func leaveChannel() {
        guard hasJoined else { return }
        engine.leaveChannel { _ in
            self.hasJoined = false
        }
    }

    deinit { /* 页面销毁时:leaveChannel 后 destroy */ }
}

十一、调试利器:开日志 + Console 控制台

1) 开 RTC 日志

ini 复制代码
swift
swift
let cfg = AgoraRtcEngineConfig()
cfg.appId = appId
cfg.channelProfile = .liveBroadcasting
cfg.logConfig.level = .info   // .debug 更详细
cfg.logConfig.filePath = NSTemporaryDirectory() + "agora_rtc.log"

2) RTM 2.x 开日志

ini 复制代码
swift
swift
let logCfg = AgoraRtmLogConfig()
logCfg.level = .info
let cfg = AgoraRtmClientConfig(appId: appId, userId: uid)
cfg.logConfig = logCfg

3) Agora Console(analytics)

官方无声排查流程建议你把 频道名 + 出问题的 uid + 时间段 ​ 记下来,用控制台里的 Agora Analytics​ 看每个用户的进房/发流/收流状态,比盲猜效率高 10 倍。


🧰 速查急救表(贴在你工位上那种)

现象 先看哪 一句定位
进房成功但没声音 mediaOptions.publishMicrophoneTrack 是不是 false / 有没有 mute
两人像在不同房间 channelId 字符串 前后空格、大小写、拼接 bug
join 返回 -102 channelId 合法性 非法字符 / nil
join 返回 -121 uid 你传了 0 但服务端又期望固定 uid
突然掉线 tokenPrivilegeWillExpire Token 到期没 renew
RTM 发消息报 -10002 login 状态机 没等 login 成功就 joinChannel
锁屏后没声音 Background Modes 没勾 Audio/AirPlay
真机无声模拟器好使 Info.plist 缺 NSMicrophoneUsageDescription
iOS 14 本地网络弹窗 RTM 版本 升 RTM ≥ 1.4.1
相关推荐
择势5 小时前
基于声网 Agora RTM + RTC SDK 实现 iOS 语音聊天室(进阶封装)
ios
择势6 小时前
基于声网 Agora RTM + RTC SDK 实现 iOS 语音聊天室——从零到可跑的指南
ios
白玉cfc6 小时前
【iOS】底层原理:类的加载
ios·objective-c·xcode
光电的一只菜鸡8 小时前
shell脚本开发技巧
开发语言·ios·swift
2501_916007479 小时前
iOS应用性能优化全面指南:从内存管理到工具使用
android·ios·性能优化·小程序·uni-app·iphone·webview
库奇噜啦呼9 小时前
【iOS】源码学习-类的加载
学习·ios·cocoa
ayqy贾杰10 小时前
我同事,40了,他vibe coding了个App
前端·ios·客户端
吠品11 小时前
Ubuntu下grep配合管道用的几个场景
ios·notepad++·iphone