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

背景介绍

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

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

核心代码如下:

//需要录音时,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: 来设置 audioSession 有效,如果调用了 setCategory:error: 来更改 audioSession,则会造成调用死循环的问题。在 iOS 底层实现中,调用 setCategory:error: 时,内部会再调用 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 来实现视频播放与音频外抛。

总结

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

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

(0)

相关推荐

  • 小程序ios音频播放没声音问题的解决

    小程序提供了录音和播放音频的能力,从基础库 1.6.0 开始支持了wx.getRecorderManager(),录音都采用wx.getRecorderManager()提供的api,播放音频文件采用wx.createInnerAudioContext()提供的api 导入录音和播放音频功能 const recorderManager = wx.getRecorderManager(); // 录音功能 const innerAudioContext = wx.createInnerAudioC

  • iOS tableView多输入框如何获取数据

    前言 难得有点空暇的时间,写写文章,一壶小茶,惬意.扯远了,言归正传. 大家在做App开发的时候,肯定遇到过在一个列表中有多个让用户填写资料的情况,类似于这样的界面: iOS 如果一个tableView中有很多的输入框,而且cell是复用的,这个还有个提交功能 我的设计思路是这样的 1.建立一个Model对象,包含要输入的所有字段, 2.在建立一个cell,有个label和textField, 3.在初始化cell的地方,根据不同的indexRow,显示cell上不同的label,例如昵称.邮箱

  • iOS整个APP实现灰色主题的示例代码

    灰色主题 背景 在一些哀悼日,清明节的时候app会实现一些灰色主题功能,部分app需求是tab首页实现灰色模式就可以,但一些需求是直接整个app都变为灰色模. 普通UI界面 web页面 xib界面 attributeText加载的htmlString页面 attachment挂件页面 实现方式 基本的实现方式1,普通页面用hook颜色方式2.web页面用注入灰色js实现方式 图片变灰 重新绘制图片变为灰色代码实现 //image类别 - (UIImage *)getGrayImage { con

  • 详解 iOS 系统中的视图动画

    动画为用户界面的状态转换提供了流畅的可视化效果, 在 iOS 中大量使用了动画效果, 包括改变视图位置. 大小. 从可视化树中删除视图, 隐藏视图等. 你可以考虑用动画效果给用户提供反馈或者用来实现有趣的特效. 在 iOS 系统中, Core Animation 提供了内置的动画支持, 创建动画不需要任何绘图的代码, 你要做的只是激发指定的动画, 接下来就交给 Core Animation 来渲染, 总之, 复杂的动画只需要几行代码就可以了. 哪些属性可以添加动画效果 根据 iOS 视图编程指南

  • 如何在 iOS 应用中添加位置信息

    最近要在 iOS 应用中添加位置信息, 需要满足的需求如下: 应用在前台时能够获取位置信息: 通过切换. Home 按键将应用切换到后台时,停止获取位置信息: 应用程序在前台运行, 直接锁定屏幕时,能够继续获取位置信息: 接下来逐步实现这三个需求. 获取设备位置信息 在 iOS 上获取位置信息是很容易的, 网上的资料也很多, 我的代码如下: // make sure location service is enabled. if (!CLLocationManager.LocationServi

  • iOS UIBezierPath实现饼状图

    本文实例为大家分享了iOS UIBezierPath实现饼状图的具体代码,供大家参考,具体内容如下 首先看效果图: 代码: #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface CircleView : UIView @property (nonatomic, copy) NSArray<NSNumber *> *valueArray; @end NS_ASSUME_NONNULL_END // #define S_W

  • iOS给border设置渐变色的方法实例

    前言 本文将从4行代码出发给一个view设置渐变色,分别会谈到几个重要的渐变色相关属性,在使用过程中有几个需要特别关注的点. 给一个view的border设置渐变色是比较高阶的用法,希望本文可以在这个方面帮助到你. 给view设置渐变色 通过4行代码就可以给一个view设置渐变色: let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) let gradientLayer = CAGradientLaye

  • iOS实现循环滚动公告栏

    本文实例为大家分享了iOS实现循环滚动公告栏的具体代码,供大家参考,具体内容如下 封装了一个继承于UIView的类,如下: #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface XtayNoticeScrollView : UIView - (instancetype)initWithFrame:(CGRect)frame titleArray:(NSArray<NSString *> *)titleArray; -

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

    背景介绍 在线教室场景下,声音是最重要的内容传输渠道之一,保障声音的稳定可靠,是在线教室质量非常重要的一环.同时在线教室里许多功能模块都与声音有关联,如何处理好各个模块间的声音冲突成为一个重要话题. AVAudioSession 在 iOS 端,说到声音的话题就绕不开 AVAudioSession.AVAudioSession 的作用是管理音频这一唯一硬件资源的分配,通过调优合适的 AVAudioSession 来适配我们的 APP 对于音频的功能需求.切换音频场景的时候,需要相应的切换 AVA

  • Javascript代码混淆综合解决方案-Javascript在线混淆器

    文章来源:javascriptOnlineObfuscator">http://www.BizStruct.cn/JavascriptOnlineObfuscator Javascript 代码混淆的目的 Javascript 是一种解释执行的脚本语言,主要应用于 Web 领域的客户端的浏览器中:由于 Javascript 解释执行的特性,代码必须明文下载到客户端,并且可以很容易的进行调试,使得 Javascript 代码的保护非常困难: 不同的人对 Javascript 代码的保护有不同

  • 微信浏览器弹出框滑动时页面跟着滑动的实现代码(兼容Android和IOS端)

    在做微信开发的时候遇到这个问题:微信浏览器弹出框滑动时页面跟着滑动. 我觉得这个问题用的是下面这几行代码: var $body = $('body'), dialogIsInView = !1,//当前是不是对话框 lastContentContainerScrollTop = -1,//用于弹出框禁止内容滚动 $contentContainer = $('#content-container');//内容容器 //阻止Window滚动 function stopWindowScroll() {

  • 阿里数据iOS端启动速度优化心得

    背景 7月26号我们阿里数据iOS端发布了4.4.0版本,这次版本主要是优化了性能,其中main()阶段的启动耗时优化成果比较明显,从之前的0.5-0.7秒,降低为目前的0.1-0.2秒(main()第一行代码到didFinishLaunchingWithOptions最后一行代码的耗时),用户体验提升明显.在这里梳理一下优化的一些经验,欢迎大家一起交流. 应用启动流程 iOS应用的启动可分为pre-main阶段和main()阶段,其中系统做的事情依次是: 1. pre-main阶段 1.1.

  • 快速解决vue在ios端下点击响应延时的问题

    在apicloud开发中遇到的问题:用vue.js的点击事件,安卓点击响应迅速而ios点击响应有延时(大约300ms). 解决方案如下: 引入<script type="text/javascript" src="/js/mobile/fastclick.js"></script> 然后在页面加载时,使用 $(function() { FastClick.attach(document.body); }); 以上这篇快速解决vue在ios端下

  • 解决IOS端微信H5页面软键盘弹起后页面下方留白的问题

    前言:微信H5项目,ios端出现了软键盘输完隐藏后页面不会回弹,下方会有一大块留白 最近微信和ios都有版本升级,不知道是哪边升级造成的,但是经过测试,软键盘收起后,再滚动一下页面,下面的留白就会消失.所以只要在输入完毕后模拟一下这个"滚动"的操作,就能解决问题了. 如果是用vue写的: <input type="text" @blur="fixScroll" placeholder="请输入xxx"/> //me

  • ios端ijkplayer编译教程

    第一步:打开IJKMediaPlayer,找到Edit Scheme,在Info选项下的Build Configuration中选择Release后关闭窗口. 第二步:分别在模拟器和真机下编译IJKMediaPlayer,并找到模拟器和真机对应的IJKMediaFramework.framework. 如下所示: 第三步:合成模拟器和真机下的framework,这样才可以在真机和模拟器下同时使用.我这里是将两个framework复制到桌面,合并的命令行如下所示: lipo -create /Us

  • 基于IOS端微信分享失效的踩坑及解决方法

    最近的一个公众号是基于vue的spa应用,在接入微信分享和微信语音的时候出现了:在Android上一切正常,但是在ios端调用wx.config的时候总是失败,去翻了官方文档也并没有找到解决方案,最后在测试中发现是因为初始化的时候传入的URL的问题.具体过程如下: 微信config接口配置,官方文档如下: 所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支

  • 详解React Native与IOS端之间的交互

    前置准备 首先最好了解一点关于 oc 的语法知识 1.创建声明文件nativeModule.h #import <Foundation/Foundation.h> #import <React/RCTBridgeModule.h> @interface nativeModule : NSObject <RCTBridgeModule> @end 2.创建文件nativeModule.m #import <Foundation/Foundation.h> #i

  • 微信小程序audio组件在ios端无法播放的解决办法

    解决方法: 给 audio 组件绑定点击事件,手动触发播放暂停方法! 代码片段: wxml文件 <!-- 判断是语音通话,有通话记录,通话描述不包含'未接' --> <view class="reference" wx:if="{{itemList.activity_type === 'phone' && itemList.activity_reference_id && tool.indexOf(itemList.comme

随机推荐