当然,这些模式只是CoreAduio总结的,不一定完全满足要求,对于具体的模式,
在iOS10中还是可以微调的。通过接口:
(BOOL )setCategory:(NSString *)category mode:(NSString *)mode options:
(AVAudioSessionCategoryOptions )options error:(NSError **)outError
但是在iOS9及以下就只能在Category上调了,其实本质是一样的,可以认为是个API糖,接口封装。
通话音量与媒体音量
一般而言,通话音量 指的是进行语音、视频通话时的音量。
媒体音量 指的是播放音乐、视频或游戏的音效、背景音的音量。
在实际使用中,两者的差异 在于,
通话音量有较好的回声消除,媒体音量有较好的声音表现力。
媒体音量可以调整到 0,而通话音量不可以。
通话音量与媒体音量只能二选一,因此需要区分系统音量走的是通话音量还是媒体音量。
系统音量走通话音量,是指在设备上调整音量时,调整的是通话音量。
媒体音量同理。媒体音量和通话音量分别属于 2 个不同的、独立的系统,一个设置不会影响到另外一个。
进入通话后,音效的播放音量由通话音量控制。退出通话后,则由媒体音量控制。 一般在教育场景下,学生作为观众拉流时,使用的媒体音量,老师说话的声音更加立体饱满,当学生连麦时,使用的通话音量,以保证通话声音的质量。
简单来说,非连麦模式下会使用媒体音量控制,连麦模式下会使用通话音量控制,两者有独立的音量控制机制。
当播放媒体资源时,使用播放器(如 AVPlayer)播放音频,播放器底层 AudioUnit 的 description 为 VoiceProcessingIO。
RTC SDK 内部维护了一个 AudioUnit,通话音量下 AudioUnit 的 description 为 RemoteIO,媒体音量下为 VoiceProcessingIO,当出现模式切换时,会销毁原来的 AudioUnit,再创建新的 AudioUnit,始终保持一个 AudioUnit 来进行音频播放。
通话音量下,AVPlayer 内 VoiceProcessingIO 的 AudioUnit 声音会被抑制。 同样的,在媒体音量下,RTC SDK 内的 AudioUnit 的 description 设置为 VoiceProcessingIO,如果此时其他模块通过设置 AVAudioSession 切换到通话音量,RTC 的声音也会被抑制。
行业现状 及 问题
在线教室场景下,很多功能都需要播放声音,包括课中音视频直播、课后回放、webview 内嵌课件声音(包括音频、视频、音效)、课堂音频、课堂视频、课堂游戏声音、音效声音等。
除此之外,教室内还包括很多需要声音录制的功能,包括连麦、跟读、集体发言、聊天语音输入、语音识别等。
教室内这些功能存在各种组合,且对 AVAudioSession 的设置要求存在差异,而 AVAudioSession 又是一个单例,如果没有一个统一管理的逻辑,很容易就出现设置混乱的问题。
目前行业内碰到的比较多的问题主要是听不见 RTC 声音与媒体声音被抑制。
听不见 RTC 声音
听不见 RTC 声音的主要原因是其他功能在设置 AVAudioSession 时,AVAudioSessionOptions 未包含 AVAudioSessionCategoryOptionMixWithOthers 混音模式,导致 RTC 声音被高优进程打断。比如在非混音模式下播放 webview 的内嵌音频,因为 webview 是使用系统进程来播放声音,优先级最高,所以 APP 进程下的 RTC 声音就会被抑制导致无法正常发声。
这类问题一般都比较隐蔽,因为简单的场景如果有问题,在上线之前一般都能测试出来,而当多个功能场景串起来之后才触发问题,往往就很难在测试期间发现,且如果线上没有完备的日志查询体系,针对线上这类问题排查起来难度也非常大,往往因为定位不到原因而长期遗留。
媒体声音被抑制
在通话音量模式下,媒体声音会被压低,导致声音变小。比较常见的场景是在小班场景下,学生在推流时播放课堂音视频等媒体资源,声音会比 RTC 的声音要小,导致媒体声音听不清楚。
通话模式下(连麦时)媒体声音会被压低,原因是 iOS 手机系统会开启回声消除以保证人声体验,因此会压低媒体通道的声音,也会压低背景音效。
教育行业内部分头部 APP 也没有从根本上解决该问题,很多都是通过从产品功能层面上规避问题,通过产品妥协来为技术问题让步。比如在播放课堂音视频资源时,默认将所有学生都强制关麦,关麦时学生处于媒体音量,就不存在被压低的问题了,等到课堂音视频播放结束后,再允许学生开麦。这种通过规避问题场景来解决问题的方式,不具有可复制性。
RTC 声音变小
RTC 声音变小,主要原因是声音通过听筒发声,而没有正常通过扬声器发声,造成声音变小的假象。 另外在 iOS14 系统下,使用过 RTC 的通话模式并切回媒体模式后,再调用 setCategory:PlayAndRecord + DefaultToSpeaker 就会必现声音小的问题。
针对上述行业痛点,通过底层原理的分析与实际项目经验,从代码规范、问题兜底、问题报警梳理出一套可行的解决方案。
听不见 RTC 声音、RTC 声音变小
RTC 的声音问题基本是因为其他模块功能对 AVAudioSession 进行了更改,且在功能结束之后,也没有将 AVAudioSession 重置到 RTC 需要的设置。本身音视频 SDK(如 agora、zego 等)
对这种情况会有一定的兜底逻辑,但是这种兜底如果存在侵入性,也是不合理的,因此具有一定的局限性。
AudioSession 修改规范
由于系统无法区分同一个进程中是哪个模块对 AudioSession 进行了更改,所以为了避免听不见 RTC 声音的问题,在使用 RTC 时,其它模块对 AudioSession 的调用更改,需要遵循以下原则:
模块调用 setCategory 前先判断下,当前 AudioSession 如已满足使用需要,不用再次设置,避免触发 iOS 14 系统 Bug
模块需要录音时,Category 应该使用 PlayAndRecord(为了防止打断正在播放的音频,不要使用仅录音的 CategoryRecord),当前 category 不是 PlayAndRecord 的情况下再调用 setCategory
模块仅需要播放时,当前 category 为 PlayAndRecord 或 Playback、Ambient 的情况下不需要 setCategory
若当前的 category 不满足模块使用,在 setCategory 之前应该先保存当前的 AudioSession 状态,然后再 setCategory、使用音频功能,使用结束后,应该重新 setCategory 恢复到之前的 AudioSession 状态
在设置 audioSession 时,categoryOptions 都应该包含 AVAudioSessionCategoryOptionDefaultToSpeaker 与 AVAudioSessionCategoryOptionMixWithOthers,iOS10 系统及以上还应包含 AVAudioSessionCategoryOptionAllowBluetooth。
核心代码如下:
if ([AVAudioSession sharedInstance].category != AVAudioSessionCategoryPlayAndRecord ) {
RTCAudioSessionCacheManager cacheCurrentAudioSession];
AVAudioSessionCategoryOptions categoryOptions = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers ;
if (@available (iOS 10.0 , * )) {
categoryOptions |= AVAudioSessionCategoryOptionAllowBluetooth ;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:categoryOptions error:nil ];
[[AVAudioSession sharedInstance] setActive:YES error:nil ];
[RTCAudioSessionCacheManager resetToCachedAudioSession];
static AVAudioSessionCategory cachedCategory = nil ;
static AVAudioSessionCategoryOptions cachedCategoryOptions = nil ;
@implementation RTCAudioSessionCacheManager
+ (void )cacheCurrentAudioSession {
if (![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayback ] && ![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayAndRecord ]) {
return ;
@synchronized (self ) {
cachedCategory = [AVAudioSession sharedInstance].category;
cachedCategoryOptions = [AVAudioSession sharedInstance].categoryOptions;
+ (void )resetToCachedAudioSession {
if (!cachedCategory || !cachedCategoryOptions) {
return ;
BOOL needResetAudioSession = ![[AVAudioSession sharedInstance].category isEqualToString:cachedCategory] || [AVAudioSession sharedInstance].categoryOptions != cachedCategoryOptions;
if (needResetAudioSession) {
dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{
[[AVAudioSession sharedInstance] setCategory:cachedCategory withOptions:cachedCategoryOptions error:nil ];
[[AVAudioSession sharedInstance] setActive:YES error:nil ];
@synchronized (self ) {
cachedCategory = nil ;
cachedCategoryOptions = nil ;
考虑到在线教室场景的复杂度,让教室内所有功能代码都遵循 AVAudioSession 的修改规范,虽然有严格的 codeReview,但是也存在一定的人为因素风险,随着业务功能不断迭代,无法完全保证线上不出问题,因此一套可靠的兜底策略显得非常有必要。
兜底策略的基本逻辑是 hook 到 AVAudioSession 的变化,当各模块对 AVAudioSession 的设置不符合规范要求时,我们在不影响功能的前提下强制进行修正,比如对 options 补充上混音模式。
通过方法交换我们可以 hook 到 AVAudioSession 的更改。比如用 kk_setCategory:withOptions: error: 与系统的 setCategory:withOptions: error: 进行交换,在交换的方法里,我们判断 options 是否包含 AVAudioSessionCategoryOptionMixWithOthers,如果没有包含我们就进行追加。
- (BOOL )kk_setCategory:(AVAudioSessionCategory )category withOptions:(AVAudioSessionCategoryOptions )options error:(NSError **)outError {
BOOL addMixWithOthersEnable = shouldFixAudioSession && !(options & AVAudioSessionCategoryOptionMixWithOthers )];
if (addMixWithOthersEnable) {
return [self kk_setCategory:category withOptions:options | AVAudioSessionCategoryOptionMixWithOthers error:outError];;
return [self kk_setCategory:category withOptions:options error:outError];
但上述方法只对通过调用 setCategory:withOptions: error: 来设置 AVAudioSession 有效,如果某个模块调用setCategory:error: 方法来设置 AVAudioSession,setCategory:error: 方法默认会将options设置为 0(未包含AVAudioSessionCategoryOptionMixWithOthers)。
我们 hook 到 setCategory:error: 方法后,无法通过调整参数的方式来为options追加混音模式选项,但是可以在交换的方法内改为调用 setCategory:withOptions:error: 方法,并将 options 参数传入AVAudioSessionCategoryOptionMixWithOthers,来满足我们的需求。
可问题在于调用 setCategory:withOptions:error: 时,底层会再嵌套调用 setCategory:error: 方法,而此时setCategory:error: 已经被我们hook并且在交换的方法内调用了setCategory:withOptions:error:,如此便形成了死循环。
针对该问题,我们通过监听 AVAudioSessionRouteChangeNotification 通知,来 hookcategory 的变化,AVAudioSessionRouteChangeNotification 在调用 setCategory:error: 时会触发,而不会在调用 setCategory:withOptions: error: 时直接触发,进而与上述方法形成了很好的互补。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (handleRouteChangeNotification:) name:AVAudioSessionRouteChangeNotification object:nil ];
- (void )handleRouteChangeNotification:(NSNotification *)notification {
NSNumber * reasonNumber =
notification.userInfo[AVAudioSessionRouteChangeReasonKey ];
AVAudioSessionRouteChangeReason reason =
(AVAudioSessionRouteChangeReason )reasonNumber.unsignedIntegerValue;
if (reason == AVAudioSessionRouteChangeReasonCategoryChange ) {
AVAudioSessionCategoryOptions currentCategoryOptions = [AVAudioSession sharedInstance].categoryOptions;
AVAudioSessionCategory currentCategory = [AVAudioSession sharedInstance].category;
if (shouldFixAudioSession && !(currentCategoryOptions & AVAudioSessionCategoryOptionMixWithOthers
)) {
[[AVAudioSession sharedInstance] setCategory:currentCategory withOptions:currentCategoryOptions | AVAudioSessionCategoryOptionMixWithOthers error:nil ];
即使有修改规范与兜底策略的保障,随着教室业务迭代与 iOS 系统升级,也无法保证线上完全不出问题,因此我们建立了问题报警机制,当线上出现问题时,能在工作群里及时收到警报,根据警报的问题信息,通过日志进一步排查问题。通过报警机制,我们可以更快速的对线上问题作出反应,不被动依赖于学生的投诉反馈,以最快的速度推进问题解决。
当 RTC 声音被打断时,底层音视频 SDK 会回调警告错误码(如 agora 的 warningCode 为 1025),当出现对应的警告码时,结合 slardar 的报警功能,在飞书群里以消息的形式进行同步。同时在 hook 到 AVAudioSession 的变更时,通过获取堆栈信息,可以定位到是哪个模块触发的更改,结合报警用户信息,可以更方便的定位问题。
媒体声音被抑制
媒体声音在媒体音量下开启播放,播放途中因为连麦而切换到了通话音量,此时因为系统特性,媒体音量会被通话音量抑制而导致声音变小。
针对该问题,我们使用音视频 SDK 提供的混音、混流功能来规避。基本原理是播放媒体资源时,我们拿到资源的 pcm 音频数据,将数据抛给 RTC 的 audioUnit 进行混合,由 RTC 音频播放单元统一播放,如果此时 RTC 使用的是通话音量,则媒体资源也是使用的通话音量播放,反之亦然。以此来保证媒体资源与 RTC 始终保持统一的音量控制机制,而避免声音大小存在差异。
混音是指给到音频的本地文件路径,或者播放的 url,由 SDK 进行数据读取与播放。混流是指针对视频文件,播放器只解码播放视频数据,将音频数据实时抛出来给到 SDK,SDK 将传入的实时音频数据与 RTC 音频数据进行混合与播放。项目中我们使用点播 SDK TTVideoEngine 来实现视频播放与音频外抛。
通过上线上述综合解决方案,声音问题得到了有效的解决,同时也能从容应对快速迭代的教室需求,有效提升了在线教室的体验。
作者:字节跳动技术团队
链接:juejin.cn/post/693498…
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2601
京东云开发者
Flutter
Android
848
Swift社区
Swift
SwiftUI