在线教室场景下,声音是最重要的内容传输渠道之一,保障声音的稳定可靠,是在线教室质量很是重要的一环。同时在线教室里许多功能模块都与声音有关联,如何处理好各个模块间的声音冲突成为一个重要话题。ios
在 iOS 端,说到声音的话题就绕不开 AVAudioSession。AVAudioSession 的做用是管理音频这一惟一硬件资源的分配,经过调优合适的 AVAudioSession 来适配咱们的 APP 对于音频的功能需求。切换音频场景的时候,须要相应的切换 AVAudioSession。web
教育场景下主要使用到的音频场景有:缓存
iOS 提供 AVAudioSessionMode 用于与 AVAudioSessionCategory 搭配使用,教育场景下使用到的音频模式主要有:markdown
咱们可使用 options 去微调 Category 行为,教育场景下经常使用的有:session
通常而言,通话音量指的是进行语音、视频通话时的音量。媒体音量指的是播放音乐、视频或游戏的音效、背景音的音量。app
在实际使用中,二者的差别在于,通话音量有较好的回声消除,媒体音量有较好的声音表现力。媒体音量能够调整到 0,而通话音量不能够。async
通话音量与媒体音量只能二选一,所以须要区分系统音量走的是通话音量仍是媒体音量。系统音量走通话音量,是指在设备上调整音量时,调整的是通话音量。媒体音量同理。媒体音量和通话音量分别属于 2 个不一样的、独立的系统,一个设置不会影响到另一个。ide
进入通话后,音效的播放音量由通话音量控制。退出通话后,则由媒体音量控制。 通常在教育场景下,学生做为观众拉流时,使用的媒体音量,老师说话的声音更加立体饱满,当学生连麦时,使用的通话音量,以保证通话声音的质量。oop
简单来讲,非连麦模式下会使用媒体音量控制,连麦模式下会使用通话音量控制,二者有独立的音量控制机制。测试
当播放媒体资源时,使用播放器(如 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 声音的主要缘由是其余功能在设置 AVAudioSession 时,AVAudioSessionOptions 未包含 AVAudioSessionCategoryOptionMixWithOthers
混音模式,致使 RTC 声音被高优进程打断。好比在非混音模式下播放 webview 的内嵌音频,由于 webview 是使用系统进程来播放声音,优先级最高,因此 APP 进程下的 RTC 声音就会被抑制致使没法正常发声。
这类问题通常都比较隐蔽,由于简单的场景若是有问题,在上线以前通常都能测试出来,而当多个功能场景串起来以后才触发问题,每每就很难在测试期间发现,且若是线上没有完备的日志查询体系,针对线上这类问题排查起来难度也很是大,每每由于定位不到缘由而长期遗留。
在通话音量模式下,媒体声音会被压低,致使声音变小。比较常见的场景是在小班场景下,学生在推流时播放课堂音视频等媒体资源,声音会比 RTC 的声音要小,致使媒体声音听不清楚。
通话模式下(连麦时)媒体声音会被压低,缘由是 iOS 手机系统会开启回声消除以保证人声体验,所以会压低媒体通道的声音,也会压低背景音效。
教育行业内部分头部 APP 也没有从根本上解决该问题,不少都是经过从产品功能层面上规避问题,经过产品妥协来为技术问题让步。好比在播放课堂音视频资源时,默认将全部学生都强制关麦,关麦时学生处于媒体音量,就不存在被压低的问题了,等到课堂音视频播放结束后,再容许学生开麦。这种经过规避问题场景来解决问题的方式,不具备可复制性。
RTC 声音变小,主要缘由是声音经过听筒发声,而没有正常经过扬声器发声,形成声音变小的假象。 另外在 iOS14 系统下,使用过 RTC 的通话模式并切回媒体模式后,再调用 setCategory:PlayAndRecord
+ DefaultToSpeaker
就会必现声音小的问题。
针对上述行业痛点,经过底层原理的分析与实际项目经验,从代码规范、问题兜底、问题报警梳理出一套可行的解决方案。
RTC 的声音问题基本是由于其余模块功能对 AVAudioSession 进行了更改,且在功能结束以后,也没有将 AVAudioSession 重置到 RTC 须要的设置。自己音视频 SDK(如 agora、zego 等)对这种状况会有必定的兜底逻辑,可是这种兜底若是存在侵入性,也是不合理的,所以具备必定的局限性。
因为系统没法区分同一个进程中是哪一个模块对 AudioSession 进行了更改,因此为了不听不见 RTC 声音的问题,在使用 RTC 时,其它模块对 AudioSession 的调用更改,须要遵循如下原则:
模块调用 setCategory
前先判断下,当前 AudioSession 如已知足使用须要,不用再次设置,避免触发 iOS 14 系统 Bug
setCategory
setCategory
若当前的 category 不知足模块使用,在 setCategory
以前应该先保存当前的 AudioSession 状态,而后再 setCategory
、使用音频功能,使用结束后,应该从新 setCategory
恢复到以前的 AudioSession 状态
在设置 audioSession 时,categoryOptions 都应该包含 AVAudioSessionCategoryOptionDefaultToSpeaker
与 AVAudioSessionCategoryOptionMixWithOthers
,iOS10 系统及以上还应包含 AVAudioSessionCategoryOptionAllowBluetooth
。
核心代码以下:
//须要录音时,AudioSession的设置代码以下:
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];
}
//功能结束时重置audioSession
[RTCAudioSessionCacheManager resetToCachedAudioSession];
复制代码
static AVAudioSessionCategory cachedCategory = nil;
static AVAudioSessionCategoryOptions cachedCategoryOptions = nil;
@implementation RTCAudioSessionCacheManager
//更改audioSession前缓存RTC当下的设置
+ (void)cacheCurrentAudioSession {
if (![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayback] && ![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) {
return;
}
@synchronized (self) {
cachedCategory = [AVAudioSession sharedInstance].category;
cachedCategoryOptions = [AVAudioSession sharedInstance].categoryOptions;
}
}
//重置到缓存的audioSession设置
+ (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;
}
});
}
}
@end
复制代码
考虑到在线教室场景的复杂度,让教室内全部功能代码都遵循 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 {
//在须要进行对audioSession进行修正的场景下(RTC直播),修改options时未包含mixWithOther,则给options追加mixWithOther
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:
时直接触发,进而与上述方法造成了很好的互补。
//添加对AVAudioSessionRouteChange的监听
[[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;
//在须要进行对audioSession进行修正的场景下(RTC直播),修改category时options未包含mixWithOther,则给options追加mixWithOther
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 来实现视频播放与音频外抛。
经过上线上述综合解决方案,声音问题获得了有效的解决,同时也能从容应对快速迭代的教室需求,有效提高了在线教室的体验。
教育技术中台团队诞生于2020年3月,咱们为字节跳动教育业务产品线提供强大的中台能力,覆盖产品包括清北网校、瓜瓜龙、大力智能灯、学浪等。咱们致力于互联网技术和教育行业的深度整合,提供高效的在线教育解决方案,知足用户多样化、个性化的教育需求。团队技术壁垒高、技术氛围浓,是提高技术竞争力的绝佳机会,期待优秀的你加入咱们!
若是你对技术充满热情,喜欢追求极致,渴望为教育事业贡献一份力量,欢迎加入咱们,咱们期待与你共同成长。咱们在北京、杭州均有招聘需求。简历投递邮箱:tech@bytedance.com,邮件标题:姓名 - 工做年限 - 教育技术中台。