音視頻開發-AVAudioSession在開發中的應用

AVAudioSession簡介

官方文檔鎮樓
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html

在這裏插入圖片描述
音頻在iOS,tvOS是一種託管服務。系統通過使用音頻會話管理應用程序、應用程序間和設備級別的音頻行爲。
我們使用AVAudioSession來與系統溝通在我們的應用程序中使用音頻。我們可以做的交互如下

  • 配置音頻會話類別(Category)和模式(mode),以便與系統通信,瞭解您打算如何在應用程序中使用音頻
  • 激活應用程序的音頻會話,將類別(Category)和模式(mode)配置放入操作中
  • 訂閱和響應重要的音頻會話通知,例如音頻中斷(AVAudioSessionInterruptionNotification)和路由更改(AVAudioSessionRouteChangeNotification)
  • 執行高級音頻設備配置,如設置採樣速率、I/O緩衝區持續時間和通道數量

AVAudioSession是App和System之間的中介,在App啓動的時候,應用程序會自動提供一個AVAudioSession單例,你通過設置這個單例來實現音頻配置。

音頻的激活

官方文檔
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/ConfiguringanAudioSession/ConfiguringanAudioSession.html#//apple_ref/doc/uid/TP40007875-CH2-SW1

音頻競爭

當我們的App啓動時,內置的應用程序或者其他App(消息、音樂、Safari、手機)可能正在後臺運行。它們中的每一個都可能產生音頻:一條文本消息到達,音樂後臺播放。

Q1:當QQ音樂後臺播放時,你的App需要播放聲音,如何處理這種競爭關係呢?

系統處理音頻競爭圖如下:
系統處理音頻競爭

背景:Music App正在佔用音頻播放。此時我們的MyApp需要進行音頻播放
1-2.MyApp請求CoreAudio控制中心,我請求激活音頻會話
//get your app's audioSession singleton object
AVAudioSession* session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord
                                             withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker |AVAudioSessionCategoryOptionAllowBluetooth
                                                   error:nil];
                                                   
[session setMode:AVAudioSessionModeDefault error:&error];
                                                   
[session setActive:YES error:&error];

3-4.通過MyApp的設置代碼,CoreAudio考慮其類別,對其他音頻進行處理
如果,您的應用程序使用了一個類別(不是AVAudioSessionCategoryOptionMixWithOthers),該類別要求對其他音頻進行靜音,系統將關閉音樂應用程序的音頻會話,停止其音頻播放。
5.系統(CoreAudio)激活您的應用程序的音頻會話和播放可以開始。

音頻打斷恢復

Q2:當你的App正在播放,此時來了一個電話,接聽電話後,如何讓App恢復音頻播放?

AVAudioSessionInterruptionNotification

https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/HandlingAudioInterruptions/HandlingAudioInterruptions.html

當電話打斷,短信打斷時,此類音頻打斷我們都會受到一個AVAudioSessionInterruptionNotification通知,通過監聽這個通知,我們處理App的音頻恢復播放

AVAudioSession *session = [AVAudioSession sharedInstance];
NSNotificationCenter *noficationCenter = [NSNotificationCenter defaultCenter];
[noficationCenter addObserver: self
                     selector: @selector(handleRouteChange:)
                         name: AVAudioSessionRouteChangeNotification
                       object: session]; //這是route改變通知,例如耳機插拔
[noficationCenter addObserver: self
                     selector: @selector(handleInterruption:)
                         name: AVAudioSessionInterruptionNotification
                       object: session]; //這是音頻打斷通知

以下是我的項目中處理打斷通知的模板,
_isAudioInteruptBegan表示打斷開始,當進入前臺時,我們會重置音頻,所以需要這個標識。即打斷開始後,App又自己進入前臺了。
_isBackground是在後臺模式下的處理,此時應區分App是否支持Audio BackgroudModes,否則在App不支持(即在後臺掛起情況),那麼會影響我們的音頻處理。
_isBackgroundAudioMode表示App是否支持Audio BackgroudModes,通過獲取plist中的字段,我們可以知道。

上面_isBackgroundAudioMode _isBackground _isAudioInteruptBegan是我自己添加的

[AVAudioSession sharedInstance].secondaryAudioShouldBeSilencedHint 指示另一應用程序是否正在播放音頻,與otherAudioPlaying一樣,只是在iOS 8之後,其判斷更加嚴格

- (void)handleInterruption:(NSNotification *)notification {
    NSInteger reason = 0;
    NSString *reasonStr = @"";
    if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) {
        //Posted when an audio interruption occurs.
        if (@available(iOS 10.3, *)) { //bug https://blog.csdn.net/shengpeng3344/article/details/83617979
            BOOL isSuspend = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionWasSuspendedKey] boolValue];
            if (isSuspend) {
                NSLog(@"AVAudioSessionInterruptionWasSuspendedKey is YES");
                return;
            }
        } else {
            // Fallback on earlier versions
        }
        reason = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] integerValue];
        if (reason == AVAudioSessionInterruptionTypeBegan ) { //在非後臺模式下  打斷處理 - 後臺打斷處理會導致底層線程卡死
            reasonStr = @"AVAudioSessionInterruptionTypeBegan";
            if (_isBackground) { //如果後臺
            
            }else{
            
            }
            _isAudioInteruptBegan = YES;
        }
        
        if (reason == AVAudioSessionInterruptionTypeEnded) {
            _isAudioInteruptBegan = NO;
            reasonStr = @"AVAudioSessionInterruptionTypeEnded";
            NSNumber *seccondReason = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];
            switch ([seccondReason integerValue]) {
                case AVAudioSessionInterruptionOptionShouldResume:{ //這裏需要恢復音頻,即Active:NO方法設置了withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation選項,則會有該值
                    reasonStr = @"AVAudioSessionInterruptionTypeEnded - AVAudioSessionInterruptionOptionShouldResume ";
                    if (_isBackground) {
                        if (_isBackgroundAudioMode && ![AVAudioSession sharedInstance].secondaryAudioShouldBeSilencedHint) {
                           
                        }else{
                        
                        }
                    }else{ 
                    //if UITextView / UITextField audio entry interruped , need reset openal
                    
                    }
                }
                    
                    // Indicates that the audio session is active and immediately ready to be used. Your app can resume the audio operation that was interrupted.
                    break;
                default:
                    break;
            }
        }
        
    }
    
    NSLog(@"handleInterruption: %@ reason %@", [notification name], reasonStr);
}

其他App調用[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];,自己的App會收到AVAudioSessionInterruptionNotification,且在音頻打斷通知AVAudioSessionInterruptionNotificationAVAudioSessionInterruptionOptionKey設置爲AVAudioSessionInterruptionOptionShouldResume,這就是當接入電話後,QQ音樂會自動恢復播放的原因。如果不設置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation,則無法做到這點。

AVAudioSessionRouteChangeNotification

另附上handleRouteChange的通知處理,在我的項目中並沒有過多涉及。

參考
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/HandlingAudioHardwareRouteChanges/HandlingAudioHardwareRouteChanges.html

- (void)handleRouteChange:(NSNotification *)notification {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSString *seccReason = @"";
    NSInteger reason = [[[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
    //  AVAudioSessionRouteDescription* prevRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey];
    switch (reason) {
        case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
            seccReason = @"The route changed because no suitable route is now available for the specified category.";
            break;
        case AVAudioSessionRouteChangeReasonWakeFromSleep:
            seccReason = @"The route changed when the device woke up from sleep.";
            break;
        case AVAudioSessionRouteChangeReasonOverride:
            seccReason = @"The output route was overridden by the app.";
            break;
        case AVAudioSessionRouteChangeReasonCategoryChange:
        {
            seccReason = @"The category of the session object changed.";
            if (![session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) {
                _isNeedResetCategory = YES;
                seccReason = @"The category of the session object changed. not 13";
            }
        }
            
            break;
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: {
            seccReason = @"The previous audio output path is no longer available.";
            AVAudioSessionRouteDescription *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey];
            for (AVAudioSessionPortDescription *output in previousRoute.outputs) {
                if (output.portType == AVAudioSessionPortHeadphones) { //耳機
                    headphonesConnected = NO;
                }
            }
        }
            break;
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable: {
            seccReason = @"A preferred new audio output path is now available.";
            for (AVAudioSessionPortDescription *output in session.currentRoute.outputs) {
                if (output.portType == AVAudioSessionPortHeadphones) { //耳機
                    headphonesConnected = YES;
                }
            }
        }
            
            break;
        case AVAudioSessionRouteChangeReasonUnknown:
        default:
            seccReason = @"The reason for the change is unknown.";
            break;
    }
    GSLog(@"handleRouteChange reason is %@", seccReason);
    
    AVAudioSessionPortDescription *input = [[session.currentRoute.inputs count] ? session.currentRoute.inputs : nil objectAtIndex:0];
    AVAudioSessionPortDescription *output = [[session.currentRoute.outputs count] ? session.currentRoute.outputs : nil objectAtIndex:0];
    if (input.portType == AVAudioSessionPortHeadsetMic) {
        
    }
    GSLog(@"inport port type is %@", input.portType);
    GSLog(@"output port type is %@", output.portType);
}

遠程控制的監聽

另外遠程控制的監聽需要 MPRemoteCommandCenter 相關的知識 https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter

Media Server Reset 媒體服務器重置

AVAudioSessionMediaServicesWereResetNotification

媒體服務器通過共享服務器進程提供音頻和其他多媒體功能。雖然很少見,但媒體服務器可以在您的應用程序處於活動狀態時重置。註冊AVAudioSessionMediaServicesWereResetNotification通知,以監視媒體服務器重置。收到通知後,您的app需要做以下操作:

  • 處理孤立的音頻對象(如播放器、錄音機、轉換器或音頻隊列)並創建新對象
  • 重置任何被跟蹤的內部音頻狀態,包括AVAudioSession的所有屬性
  • 在適當的時候,使用setActive:error:方法重新激活AVAudioSession實例

重要提示:應用程序不需要重新註冊任何AVAudioSession通知或重置AVAudioSession屬性上的key-value observer。

AVAudioSessionMediaServicesWereLostNotification

如果您想知道媒體服務器何時首次不可用,還可以註冊AVAudioSessionMediaServicesWereLostNotification通知。然而,大多數應用程序只需要響應重置通知。只有當應用程序必須響應在媒體服務器丟失後但在媒體服務器重置之前發生的用戶事件時,才使用丟失的通知。

如何測試:您可以通過在“設置”->“開發者”->Reset Media Services模擬。使用此實用程序可以方便地確保您的應用程序在重置媒體服務時作出適當的響應。

Mode的影響

關於Category和Mode的配置參數,官方文檔說明了
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10-SW3

對於AVAudioSessionModeDefaultAVAudioSessionModeVoiceChat兩種模式,AVAudioSessionModeVoiceChat會隱式的降低聲音大小,同樣在AVAudioSessionModeDefault上播放的音頻聲音,在AVAudioSessionModeVoiceChat會有明顯的縮小需要開發者注意,並在合適的時機判斷Mode是否符合預期,不符合預期則重置,例如:

if (session.mode != AVAudioSessionModeDefault) {
        success = [session setMode:AVAudioSessionModeDefault error:&error];
        if (!success) MLog(@"AVAudioSession setMode error: %@",error);
    }

爲AirPlay選擇類別和模式

只有特定的類別和模式支持AirPlay。以下類別同時支持AirPlay的鏡像和非鏡像版本

  • AVAudioSessionCategorySoloAmbient
  • AVAudioSessionCategoryAmbient
  • AVAudioSessionCategoryPlayback

AVAudioSessionCategoryPlayAndRecord類別和以下模式只支持AirPlay的鏡像版本:

  • AVAudioSessionModeDefault
  • AVAudioSessionModeVideoChat
  • AVAudioSessionModeGameChat

權限獲取-錄製音頻的許可

爲了保護用戶隱私,您的應用程序在錄製音頻之前必須徵得用戶的同意。如果用戶不授予權限,則只記錄靜默。當你使用一個支持記錄的類別,而應用程序試圖使用輸入路徑時,系統會自動提示用戶獲得許可。

您可以使用requestRecordPermission:方法手動請求權限,而不是等待系統提示用戶記錄權限。使用這種方法,您的應用程序可以在不中斷應用程序的自然流程的情況下獲得許可,從而獲得更好的用戶體驗。

AVAudioSession.sharedInstance().requestRecordPermission { granted in
    if granted {
        // User granted access. Present recording interface.
    } else {
        // Present message to user indicating that recording
        // can't be performed until they change their preference
        // under Settings -> Privacy -> Microphone
    }
}

重要提示:從iOS 10開始,所有訪問設備麥克風的應用程序都必須靜態聲明它們的意圖。要做到這一點,應用程序現在必須在其信息中包含NSMicrophoneUsageDescription鍵。併爲該鍵提供一個目的字符串。當系統提示用戶允許訪問時,此字符串將作爲警告的一部分顯示。
如果一個應用程序試圖在沒有該密鑰和值的情況下訪問設備的任何麥克風,該應用程序將終止。
即應該是在plist中需要進行設置鍵值對

如果一個應用程序試圖在沒有該密鑰和值的情況下訪問設備的任何麥克風,該應用程序將終止。

關於如何打開後臺Audio模式

OS和tvOS應用程序要求您爲某些後臺操作啓用某些功能。回放應用程序需要的一個常見功能是播放背景音頻。啓用此功能後,當用戶切換到其他應用程序或鎖定iOS設備時,應用程序的音頻可以繼續。這一功能也需要支持先進的播放功能,如AirPlay流媒體和圖片中的圖片在iOS中播放。

配置這些功能的最簡單方法是使用Xcode。在Xcode中選擇應用程序的目標並選擇capability選項卡。在capability選項卡下,將背景模式切換爲ON,並從可用模式列表中選擇“Audio, AirPlay, and Picture in Picture”選項。
在這裏插入圖片描述

官方指南

音頻指南按應用程序類型

以下內容都摘自官方文檔
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioGuidelinesByAppType/AudioGuidelinesByAppType.html#//apple_ref/doc/uid/TP40007875-CH11-SW1
本人加上了自己的理解,翻譯水平有限

遊戲類應用程序

大多數遊戲都需要用戶交互才能讓遊戲中發生任何事情。在設計遊戲時使用AVAudioSessionCategoryAmbient或AVAudioSessionCategorySoloAmbient類別。當用戶打開另一個應用程序或鎖定屏幕時,他們不希望該應用程序繼續播放。通常他們希望在遊戲程序播放時,來自另一個應用程序的音頻能夠繼續播放。
以下是官方文檔的指南:

  • 在應用程序委託的applicationDidBecomeActive:方法中激活音頻會話。
  • 播放應用程序的音效,同時允許其他應用程序的音頻播放。Tips: 一般使用AVAudioSessionCategoryOptionDuckOthers
  • 當其他音頻不播放時,播放應用程序原聲音頻,否則允許前一個音頻播放。
  • 在結束中斷事件後,始終嘗試重新激活和恢復聲音效果的播放。(AVAudioSessionInterruptionNotification)
  • 查詢音頻會話的secondaryAudioShouldBeSilencedHint屬性,以確定是否應該重新播放遊戲的原聲帶。
  • 忽略所有路由更改,除非應用程序特別需要注意它們。
  • 在應用程序啓動顯示視頻閃屏前設置音頻類別。

播放和錄製類應用程序

播放和錄製類App,應該選用AVAudioSessionCategoryRecord、AVAudioSessionCategoryPlayAndRecord或AVAudioSessionCategoryPlayback類別,當它們的音頻會話被激活時,通常會中斷其他系統音頻。
以下是官方文檔的指南:

  • 當應用程序進入前臺時,等待用戶按下播放或錄製按鈕,然後激活音頻會話。
  • 當應用程序在前臺時,保持音頻會話活動,除非它被中斷。
  • 如果App在切換到後臺時沒有積極地播放或錄製音頻,則禁用其音頻會話。這可以防止它的音頻會話被另一個不可混合的App或系統在App被掛起時中斷。當你的app是非後臺模式時,你需要在進入後臺時,顯式的關閉AudioSession,否則會收到不必要的打斷通知,詳情見: https://blog.csdn.net/shengpeng3344/article/details/83617979
  • 更新UI,以指示回放或錄製在中斷時已暫停。不要停用音頻會話。
  • 觀察AVAudioSessionInterruptionNotification類型的通知,以便在音頻會話中斷時得到通知。當中斷結束時,不要再開始播放或錄製音頻,除非應用程序在中斷之前已經這樣做了
  • 如果路由更改是由拔出事件引起的,則暫停播放或錄製,但要保持音頻會話處於活動狀態。
  • 假設應用程序的音頻會話在從暫停狀態過渡到前臺狀態時處於非活動狀態。當用戶按下播放或錄製按鈕時,重新激活音頻會話。
  • 確保設置了audio UIBackgroundModes標誌,即後臺允許音頻播放。
  • 註冊遠程控制事件(請參閱[MPRemoteCommandCenter](https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter)),併爲您的媒體提供適當的NowPlaying信息(請參閱MPNowPlayingInfoCenter)。
  • 使用MPVolumeView對象來顯示系統音量調節和路由選擇器。
  • 使用後臺任務而不是流媒體靜音來防止應用程序被暫停。
  • 通過requestRecordPermission:來向用戶請求記錄權限。不要依賴操作系統來提示用戶。
  • 對於錄製應用程序,使用AVAudioSessionCategoryPlayAndRecord類別而不是AVAudioSessionCategoryRecord類別。只記錄的類別實際上會使所有系統輸出靜默,而且通常對大多數應用程序來說限制太大。

VoIP和聊天應用程序

VoIP和聊天應用程序要求輸入和輸出路由(input and output routes)都可用。這類應用程序使用AVAudioSessionCategoryPlayAndRecord類別,不與其他應用程序混合。

  • 只有當用戶接聽或發起呼叫時,才激活音頻會話。
  • 在收到中斷通知之後更新UI以反映調用的音頻已被中斷。
  • 在中斷之後,不要激活音頻會話,直到用戶回答或發起一個調用。
  • 在調用結束後,使用AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation常量禁用音頻會話。

Tips:意爲調用[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];,會告訴CoreAudio自己的App音頻關閉了,CoreAudio會恢復其他需要播放的App,這就是當接入電話後,QQ音樂會自動恢復播放的原因。如果不設置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation,則無法做到這點。

  • 忽略所有路由更改,除非應用程序特別需要注意它們。例如,路由更改可能導致對會話的採樣率、緩衝區持續時間或延遲的更改。如果這些值與您的應用程序相關,請在路由更改後查詢它們,以獲得它們的最新狀態。
  • 對於VoIP應用程序,使用蘋果的語音處理I/O音頻單元。
  • 確保設置了audio UIBackgroundModes標誌。
  • 使用MPVolumeView對象來顯示系統音量調節和路由選擇器。
  • 通過requestRecordPermission:來向用戶請求記錄權限。不要依賴操作系統來提示用戶。

從iOS 10開始,要構建與內置電話應用(電話和FaceTime應用)具有相同功能的VoIP應用程序,請使用CallKit框架。有關更多信息,請參見CallKit框架參考

計量的應用程序

計量應用程序需要對輸入和輸出路由應用最少的系統提供的信號處理。設置AVAudioSessionCategoryPlayAndRecord類別和測量模式,以最小化信號處理。此外,這類應用程序不會與其他應用程序混合。

  • 總是嘗試在結束中斷事件後重新激活並恢復播放。
  • 忽略所有路由更改,除非應用程序特別需要注意它們。
  • 在應用程序啓動顯示視頻閃屏前設置音頻類別。
  • 通過requestRecordPermission:來向用戶請求記錄權限。不要依賴操作系統來提示用戶。

類似瀏覽器的應用程序

社交媒體或其他類似瀏覽器的應用程序經常播放短視頻。它們使用AVAudioSessionCategoryPlayback類別,不服從鈴聲開關。這些應用程序也不與其他應用程序混合。

  • 總是等待用戶開始回放。
  • 在視頻結束後,使用AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation常量禁用音頻會話。
  • 由於斷開事件導致路由更改,請暫停音頻會話,但要保持音頻會話處於活動狀態。
  • 視頻播放時註冊遠程控制事件,視頻結束時註銷。
  • 當應用程序收到開始中斷事件時更新UI。
  • 在接收到結束中斷事件後,等待用戶開始回放。

導航和健身應用

導航和鍛鍊應用程序使用AVAudioSessionCategoryPlaybackAVAudioSessionCategoryPlayAndRecord類別。這些應用程序的音頻通常由簡短的語音提示組成。當播放時,這些提示會中斷語音,比如播客或有聲書,並與(和鴨子)其他音頻混合,比如音樂應用程序的播放。

  • 使用AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthersAVAudioSessionCategoryOptionDuckOthers選項激活音頻會話。即開啓混合,或者使得其他App的音頻變小
  • 在需要提示之前不要激活音頻會話。
  • 總是在播放提示後禁用音頻會話。
  • 不要試圖重新播放被中斷的提示符。

合作音樂應用程序

合作音樂應用程序是爲在其他應用程序播放時播放而設計的。這些類型的應用程序使用AVAudioSessionCategoryPlayback或AVAudioSessionCategoryPlayAndRecord`類別,並與其他應用程序混合使用。

  • 使用AVAudioSessionCategoryOptionMixWithOthers選項激活音頻會話。
  • 如果應用程序的UI沒有提供開始/停止按鈕,請遵循遊戲應用程序指南。
  • 如果應用程序的UI提供了一個start/stop按鈕,那麼只在用戶按下play按鈕時激活音頻會話。
  • 不要註冊遠程控制事件。
  • 確保設置了audio UIBackgroundModes標誌。
  • 通過requestRecordPermission:來向用戶請求記錄權限。不要依賴操作系統來提示用戶。

學習不斷,有錯誤請指出,如果對你有幫助,點個👍

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章