在線教室場景下,聲音是最重要的內容傳輸渠道之一,保障聲音的穩定可靠,是在線教室質量很是重要的一環。同時在線教室裏許多功能模塊都與聲音有關聯,如何處理好各個模塊間的聲音衝突成爲一個重要話題。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,郵件標題:姓名 - 工做年限 - 教育技術中臺。