讨论在线教室 iOS 端声音问题综合解决方案

2022-07-23,,,,

背景介绍

在线教室场景下,声音是最重要的内容传输渠道之一,保障声音的稳定可靠,是在线教室质量非常重要的一环。同时在线教室里许多功能模块都与声音有关联,如何处理好各个模块间的声音冲突成为一个重要话题。

avaudiosession

在 ios 端,说到声音的话题就绕不开 avaudiosession。avaudiosession 的作用是管理音频这一唯一硬件资源的分配,通过调优合适的 avaudiosession 来适配我们的 app 对于音频的功能需求。切换音频场景的时候,需要相应的切换 avaudiosession。

 avaudiosessioncategory

教育场景下主要使用到的音频场景有:

avaudiosessionmode

ios 提供 avaudiosessionmode[1] 用于与 avaudiosessioncategory[2] 搭配使用,教育场景下使用到的音频模式主要有:

 avaudiosessionoptions

我们可以使用 options 去微调 category 行为,教育场景下常用的有:

通话音量与媒体音量

一般而言,通话音量指的是进行语音、视频通话时的音量。媒体音量指的是播放音乐、视频或游戏的音效、背景音的音量。

在实际使用中,两者的差异在于,通话音量有较好的回声消除,媒体音量有较好的声音表现力。媒体音量可以调整到 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 的调用更改,需要遵循以下原则:

  1. 模块调用 setcategory 前先判断下,当前 audiosession 如已满足使用需要,不用再次设置,避免触发 ios 14 系统 bug
  2. 模块需要录音时,category 应该使用 playandrecord(为了防止打断正在播放的音频,不要使用仅录音的 categoryrecord),当前 category 不是 playandrecord 的情况下再调用 setcategory
  3. 模块仅需要播放时,当前 category 为 playandrecord 或 playback、ambient 的情况下不需要 setcategory
  4. 若当前的 category 不满足模块使用,在 setcategory 之前应该先保存当前的 audiosession 状态,然后再 setcategory、使用音频功能,使用结束后,应该重新 setcategory 恢复到之前的 audiosession 状态
  5. 在设置 audiosession 时,categoryoptions 都应该包含 avaudiosessioncategoryoptiondefaulttospeakeravaudiosessioncategoryoptionmixwithothers,ios10 系统及以上还应包含 avaudiosessioncategoryoptionallowbluetooth

核心代码如下:

兜底策略

考虑到在线教室场景的复杂度,让教室内所有功能代码都遵循 avaudiosession 的修改规范,虽然有严格的 codereview,但是也存在一定的人为因素风险,随着业务功能不断迭代,无法完全保证线上不出问题,因此一套可靠的兜底策略显得非常有必要。

兜底策略的基本逻辑是 hook 到 avaudiosession 的变化,当各模块对 avaudiosession 的设置不符合规范要求时,我们在不影响功能的前提下强制进行修正,比如对 options 补充上混音模式。

通过方法交换我们可以 hook 到 avaudiosession 的更改。比如用 kk_setcategory:withoptions: error: 与系统的 setcategory:withoptions: error: 进行交换,在交换的方法里,我们判断 options 是否包含 avaudiosessioncategoryoptionmixwithothers,如果没有包含我们就进行追加。

但上述方法只对通过调用 setcategory:withoptions: error: 来设置 audiosession 有效,如果调用了 setcategory:error: 来更改 audiosession,则会造成调用死循环的问题。在 ios 底层实现中,调用 setcategory:error: 时,内部会再调用 setcategory:withoptions: error: 方法,因为进行了方法交换,从而出现嵌套调用问题。

针对该问题,我们通过监听 avaudiosessionroutechangenotification 通知,来 hookcategory 的变化,avaudiosessionroutechangenotification 在调用 setcategory:error: 时会触发,而不会在调用 setcategory:withoptions: error: 时直接触发,进而与上述方法形成了很好的互补。

报警机制

即使有修改规范与兜底策略的保障,随着教室业务迭代与 ios 系统升级,也无法保证线上完全不出问题,因此我们建立了问题报警机制,当线上出现问题时,能在工作群里及时收到警报,根据警报的问题信息,通过日志进一步排查问题。通过报警机制,我们可以更快速的对线上问题作出反应,不被动依赖于学生的投诉反馈,以最快的速度推进问题解决。

当 rtc 声音被打断时,底层音视频 sdk 会回调警告错误码(如 agora 的 warningcode 为 1025),当出现对应的警告码时,结合 slardar 的报警功能,在飞书群里以消息的形式进行同步。同时在 hook 到 avaudiosession 的变更时,通过获取堆栈信息,可以定位到是哪个模块触发的更改,结合报警用户信息,可以更方便的定位问题。

媒体声音被抑制

媒体声音在媒体音量下开启播放,播放途中因为连麦而切换到了通话音量,此时因为系统特性,媒体音量会被通话音量抑制而导致声音变小。

针对该问题,我们使用音视频 sdk 提供的混音、混流功能来规避。基本原理是播放媒体资源时,我们拿到资源的 pcm 音频数据,将数据抛给 rtc 的 audiounit 进行混合,由 rtc 音频播放单元统一播放,如果此时 rtc 使用的是通话音量,则媒体资源也是使用的通话音量播放,反之亦然。以此来保证媒体资源与 rtc 始终保持统一的音量控制机制,而避免声音大小存在差异。

混音是指给到音频的本地文件路径,或者播放的 url,由 sdk 进行数据读取与播放。混流是指针对视频文件,播放器只解码播放视频数据,将音频数据实时抛出来给到 sdk,sdk 将传入的实时音频数据与 rtc 音频数据进行混合与播放。项目中我们使用点播 sdk ttvideoengine 来实现视频播放与音频外抛。

总结

通过上线上述综合解决方案,声音问题得到了有效的解决,同时也能从容应对快速迭代的教室需求,有效提升了在线教室的体验。

到此,这篇关于讨论在线教室 ios 端声音问题综合解决方案的文章就介绍到这了,更多相关在线教室ios端声音解决方案内容,请搜索以前的文章或继续浏览下面的相关文章,希望大家以后多多支持!

《讨论在线教室 iOS 端声音问题综合解决方案.doc》

下载本文的Word格式文档,以方便收藏与打印。