概述
iOS 上「声音从哪出」由AudioRouteSession.shared 管理。它只在 iOS 存在——macOS 是独立的输入 / 输出硬件设备模型,走 设备管理 的 setOutputDevice(_:)。
这套 API 的设计基准是主流 RTC SDK(声网 Agora、腾讯 TRTC、LiveKit)的通行做法,有三条需要先理解的原则,否则很容易写出「设置了但没生效」的代码。
原则一:只能切扬声器和听筒
AudioRouteTarget 只有两个值:
- 插入时系统自动切过去,多个设备同时连接时取最后连接的那个
- 拔出时 SDK 会回落到你设置的路由
- 需要让用户主动选择蓝牙 / AirPlay 时,用系统提供的
AVRoutePickerView
原则二:持久设置与临时设置分两层
优先级是 临时 > 持久。外设拔出后,SDK 按这个优先级回落。
持久设置:入会前定好默认行为
临时设置:通话中切换
原则三:入会即建立音频通道
加入频道时 SDK 就会建立通话音频通道并保持到离会,无论你有没有开麦。开不开麦只决定「推不推流」,不影响底层音频采集是否运行。 这带来两个必须提前告知产品和用户的结果: 为什么必须这样做?因为 iOS 上音频会话不处于 VoIP 通话模式时,路由控制整体不可靠——听筒输出只在.playAndRecord 会话类别下存在,而按需临时升级类别经真机验证是切不过去的。腾讯 TRTC 文档记录了同一现象:不开麦时走媒体通道,此时无法设置外放或听筒。
如果用户拒绝了麦克风权限,SDK 会降级到只播放模式:仍然可以听到对方,但听筒路由不可用,只能外放。
建议在 Info.plist 中同时声明后台音频能力,避免切到后台时音频被系统挂起:
查询当前状态
currentRoute 是只读的五态类型,包含 SDK 无法主动切换但需要如实上报的外设:
注意区分
currentRoute(实际从哪出声,五态)和 effectiveRouteTarget(你要求的目标,两态)。插着耳机时前者是 .bluetooth,后者可能仍是 .speaker——这不是矛盾,是外设优先。UI 应该展示 currentRoute。监听路由变化
reason 参数值得关注,它区分了变化来源:.oldDeviceUnavailable 是拔出,.newDeviceAvailable 是插入,.override 是 App 主动覆盖。排查路由问题时这一项往往比结果本身更有价值。
中断与系统来电
SDK 通过 CallKit 感知系统通话状态,中断恢复不是一次性的:- 音频中断结束时,如果系统电话还没真正挂断,不会立即恢复——此时重激活音频会话必定失败
- 恢复条件是「系统通话已结束」且「App 在前台」,不满足则保留待恢复状态
- 会在回到前台时、以及通话结束若干秒后自动重试
- 真正恢复成功后才触发
audioRouteSessionDidRecoverFromInterruption
与 DeviceManager 的关系
DeviceManager 上的 iOS 音频方法是这套 API 的薄封装,语义对齐 Agora 的 setEnableSpeakerphone:
DeviceManager.audioInputs() 和 setAudioInputDevice(_:) 是输入端口层面的低阶接口,与「声音从哪出」是两件事,且不参与本文的回落策略。控制输出走向请使用 AudioRouteSession。完整示例
常见问题
切换没有生效 按顺序排查:- 当前是否走在外设上(
currentRoute.isExternal)——这种情况下切扬声器本就无效 - 是否已入会或已开始采集(
isEngaged)——未建立音频通道时设置只会被记录,等通道建立后套用 - 麦克风权限是否被拒——被拒时会话降级,听筒不可用
availableInputs 也不真实。音频路由必须在真机上验证。