目录
- 【无声/单边无声】------ 占问题的 60%+
- 【Token 相关报错】------ 进房失败、中途断连
- 【RTC 错误码速查】------
-102 / -121 / -17 / -7 / -3等 - 【RTM 登录/发消息报错】------
NOT_LOGIN / TOKEN_EXPIRED / TIMEOUT / REJECTED - 【RTM 断线重连状态机】------ 你到底该不该手动 re-login
- 【麦克风权限 & iOS 隐私弹窗】------ 真机静默失败的高危坑
- 【音频路由错乱】------ 插耳机/连蓝牙后声音跑到听筒
- 【后台/锁屏后没声音】------ iOS 系统限制
- 【iOS 14 本地网络弹窗】------ RTM SDK 经典惊喜
- 【内存/生命周期】------
sharedEngine重复初始化、没 leaveChannel 就 rejoin - 【调试利器】------ 开日志 + 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.plist的 NSMicrophoneUsageDescription。
完整合规检查清单
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 之前 调,setEnableSpeakerphone在 join 之后调,调反了不生效。
如果用户的蓝牙耳机优先级更高,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 触发本地网络权限弹窗。
解决方案(二选一)
- 升 RTM SDK ≥ 1.4.1(官方修了,弹窗不再出现,服务不受影响)
- 或在
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 |