前言
在實施前一篇中所述的7個步驟步之前還必須面對一個麻煩的問題,Audiosession。 AudioSession簡介AudioSession這個玩意的主要功能包括以下幾點(圖片來自官方文檔): 1. 確定你的app如何使用音頻(是播放?還是錄音?)2. 為你的app選擇合適的輸入輸出設備(比如輸入用的麥克風,輸出是耳機、手機功放或者airplay)3. 協調你的app的音頻播放和系統以及其他app行為(例如有電話時需要打斷,電話結束時需要恢復,按下靜音按鈕時是否歌曲也要靜音等) AudioSession AudioSession相關的類有兩個:1. AudioToolBox中的AudioSession2. AVFoundation中的AVAudioSession 其中AudioSession在SDK 7中已經被標注為dePRacated,而AVAudioSession這個類雖然iOS 3開始就已經存在了,但其中很多方法和變量都是在iOS 6以后甚至是iOS 7才有的。所以各位可以依照以下標準選擇: * 如果最低版本支持iOS 5,可以使用AudioSession,也可以使用AVAudioSession;* 如果最低版本支持iOS 6及以上,請使用AVAudioSession 下面以AudioSession類為例來講述AudioSession相關功能的使用(很不幸我需要支持iOS 5。。T-T,使用AVAudioSession的同學可以在其頭文件中尋找對應的方法使用即可,需要注意的點我會加以說明)。 注意:在使用AVAudioPlayer/AVPlayer時可以不用關心AudioSession的相關問題,Apple已經把AudioSession的處理過程封裝了,但音樂打斷后的響應還是要做的(比如打斷后音樂暫停了UI狀態也要變化,這個應該通過KVO就可以搞定了吧。。我沒試過瞎猜的>_<)。 初始化AudioSession使用AudioSession類首先需要調用初始化方法:
AudioSession AudioSession相關的類有兩個:1. AudioToolBox中的AudioSession2. AVFoundation中的AVAudioSession 其中AudioSession在SDK 7中已經被標注為dePRacated,而AVAudioSession這個類雖然iOS 3開始就已經存在了,但其中很多方法和變量都是在iOS 6以后甚至是iOS 7才有的。所以各位可以依照以下標準選擇: * 如果最低版本支持iOS 5,可以使用AudioSession,也可以使用AVAudioSession;* 如果最低版本支持iOS 6及以上,請使用AVAudioSession 下面以AudioSession類為例來講述AudioSession相關功能的使用(很不幸我需要支持iOS 5。。T-T,使用AVAudioSession的同學可以在其頭文件中尋找對應的方法使用即可,需要注意的點我會加以說明)。 注意:在使用AVAudioPlayer/AVPlayer時可以不用關心AudioSession的相關問題,Apple已經把AudioSession的處理過程封裝了,但音樂打斷后的響應還是要做的(比如打斷后音樂暫停了UI狀態也要變化,這個應該通過KVO就可以搞定了吧。。我沒試過瞎猜的>_<)。 初始化AudioSession使用AudioSession類首先需要調用初始化方法:extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioSessionInterruptionListener inInterruptionListener, void *inClientData); 前兩個參數一般填NULL表示AudioSession運行在主線程上(但并不代表音頻的相關處理運行在主線程上,只是AudioSession),第三個參數需要傳入一個一個AudioSessionInterruptionListener類型的方法,作為AudioSession被打斷時的回調,第四個參數則是代表打斷回調時需要附帶的對象(即回到方法中的inClientData,如下所示,可以理解為UIView animation中的context)。typedef void (*AudioSessionInterruptionListener)(void * inClientData, UInt32 inInterruptionState); 這才剛開始,坑就來了。這里會有兩個問題: 第一,AudioSessionInitialize可以被多次執行,但AudioSessionInterruptionListener只能被設置一次,這就意味著這個打斷回調方法是一個靜態方法,一旦初始化成功以后所有的打斷都會回調到這個方法,即便下一次再次調用AudioSessionInitialize并且把另一個靜態方法作為參數傳入,當打斷到來時還是會回調到第一次設置的方法上。 這種場景并不少見,例如你的app既需要播放歌曲又需要錄音,當然你不可能知道用戶會先調用哪個功能,所以你必須在播放和錄音的模塊中都調用AudioSessionInitialize注冊打斷方法,但最終打斷回調只會作用在先注冊的那個模塊中,很蛋疼吧。。。所以對于AudioSession的使用最好的方法是生成一個類單獨進行管理,統一接收打斷回調并發送自定義的打斷通知,在需要用到AudioSession的模塊中接收通知并做相應的操作。 Apple也察覺到了這一點,所以在AVAudioSession中首先取消了Initialize方法,改為了單例方法sharedInstance。在iOS 5上所有的打斷都需要通過設置id<AVAudioSessionDelegate> delegate并實現回調方法來實現,這同樣會有上述的問題,所以在iOS 5使用AVAudioSession下仍然需要一個單獨管理AudioSession的類存在。在iOS 6以后Apple終于把打斷改成了通知的形式。。這下科學了。 第二,AudioSessionInitialize方法的第四個參數inClientData,也就是回調方法的第一個參數。上面已經說了打斷回調是一個靜態方法,而這個參數的目的是為了能讓回調時拿到context(上下文信息),所以這個inClientData需要是一個有足夠長生命周期的對象(當然前提是你確實需要用到這個參數),如果這個對象被dealloc了,那么回調時拿到的inClientData會是一個野指針。就這一點來說構造一個單獨管理AudioSession的類也是有必要的,因為這個類的生命周期和AudioSession一樣長,我們可以把context保存在這個類中。 監聽RouteChange事件如果想要實現類似于“拔掉耳機就把歌曲暫停”的功能就需要監聽RouteChange事件:extern OSStatus AudioSessionAddPropertyListener(AudioSessionPropertyID inID, AudioSessionPropertyListener inProc, void *inClientData); typedef void (*AudioSessionPropertyListener)(void * inClientData, AudioSessionPropertyID inID, UInt32 inDataSize, const void * inData); 調用上述方法,AudioSessionPropertyID參數傳kAudioSessionProperty_AudioRouteChange,AudioSessionPropertyListener參數傳對應的回調方法。inClientData參數同AudioSessionInitialize方法。 同樣作為靜態回調方法還是需要統一管理,接到回調時可以把第一個參數inData轉換成CFDictionaryRef并從中獲取kAudioSession_AudioRouteChangeKey_Reason鍵值對應的value(應該是一個CFNumberRef),得到這些信息后就可以發送自定義通知給其他模塊進行相應操作(例如kAudioSessionRouteChangeReason_OldDeviceUnavailable就可以用來做“拔掉耳機就把歌曲暫停”)。//AudioSession的AudioRouteChangeReason枚舉 enum { kAudioSessionRouteChangeReason_Unknown = 0, kAudioSessionRouteChangeReason_NewDeviceAvailable = 1, kAudioSessionRouteChangeReason_OldDeviceUnavailable = 2, kAudioSessionRouteChangeReason_CategoryChange = 3, kAudioSessionRouteChangeReason_Override = 4, kAudioSessionRouteChangeReason_WakeFromSleep = 6, kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7, kAudioSessionRouteChangeReason_RouteConfigurationChange = 8 };//AVAudioSession的AudioRouteChangeReason枚舉 typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason) { AVAudioSessionRouteChangeReasonUnknown = 0, AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1, AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2, AVAudioSessionRouteChangeReasonCategoryChange = 3, AVAudioSessionRouteChangeReasonOverride = 4, AVAudioSessionRouteChangeReasonWakeFromSleep = 6, AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7, AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8 } 注意:iOS 5下如果使用了AVAudioSession由于AVAudioSessionDelegate中并沒有定義相關的方法,還是需要用這個方法來實現監聽。iOS 6下直接監聽AVAudioSession的通知就可以了。 這里附帶兩個方法的實現,都是基于AudioSession類的(使用AVAudioSession的同學幫不到你們啦)。 1、判斷是否插了耳機:+ (BOOL)usingHeadset { #if TARGET_ipHONE_SIMULATOR return NO; #endif CFStringRef route; UInt32 propertySize = sizeof(CFStringRef); AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route); BOOL hasHeadset = NO; if((route == NULL) || (CFStringGetLength(route) == 0)) { // Silent Mode } else { /* Known values of route: * "Headset" * "Headphone" * "Speaker" * "SpeakerAndMicrophone" * "HeadphonesAndMicrophone" * "HeadsetInOut" * "ReceiverAndMicrophone" * "Lineout" */ NSString* routeStr = (__bridge NSString*)route; NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"]; NSRange headsetRange = [routeStr rangeOfString : @"Headset"]; if (headphoneRange.location != NSNotFound) { hasHeadset = YES; } else if(headsetRange.location != NSNotFound) { hasHeadset = YES; } } if (route) { CFRelease(route); } return hasHeadset; } 2、判斷是否開了Airplay(來自StackOverflow):+ (BOOL)isAirplayActived { CFDictionaryRef currentRouteDescriptionDictionary = nil; UInt32 dataSize = sizeof(currentRouteDescriptionDictionary); AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription, &dataSize, ¤tRouteDescriptionDictionary); BOOL airplayActived = NO; if (currentRouteDescriptionDictionary) { CFArrayRef outputs = CFDictionaryGetValue(currentRouteDescriptionDictionary, kAudioSession_AudioRouteKey_Outputs); if(outputs != NULL && CFArrayGetCount(outputs) > 0) { CFDictionaryRef currentOutput = CFArrayGetValueAtIndex(outputs, 0); //Get the output type (will show airplay / hdmi etc CFStringRef outputType = CFDictionaryGetValue(currentOutput, kAudioSession_AudioRouteKey_Type); airplayActived = (CFStringCompare(outputType, kAudioSessionOutputRoute_AirPlay, 0) == kCFCompareEqualTo); } CFRelease(currentRouteDescriptionDictionary); } return airplayActived; } 設置類別下一步要設置AudioSession的Category,使用AudioSession時調用下面的接口extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID, UInt32 inDataSize, const void *inData); 如果我需要的功能是播放,執行如下代碼UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback; AudioSessionSetProperty (kAudioSessionProperty_AudioCategory, sizeof(sessionCategory), &sessionCategory); 使用AVAudioSession時調用下面的接口/* set session category */ - (BOOL)setCategory:(NSString *)category error:(NSError **)outError; /* set session category with options */ - (BOOL)setCategory:(NSString *)category withOptions: (AVAudioSessionCategoryOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0); 至于Category的類型在官方文檔中都有介紹,我這里也只羅列一下具體就不贅述了,各位在使用時可以依照自己需要的功能設置Category。//AudioSession的AudioSessionCategory枚舉 enum { kAudioSessionCategory_AmbientSound = 'ambi', kAudioSessionCategory_SoloAmbientSound = 'solo', kAudioSessionCategory_MediaPlayback = 'medi', kAudioSessionCategory_RecordAudio = 'reca', kAudioSessionCategory_PlayAndRecord = 'plar', kAudioSessionCategory_AudioProcessing = 'proc' };//AudioSession的AudioSessionCategory字符串 /* Use this category for background sounds such as rain, car engine noise, etc. Mixes with other music. */ AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient; /* Use this category for background sounds. Other music will stop playing. */ AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient; /* Use this category for music tracks.*/ AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback; /* Use this category when recording audio. */ AVF_EXPORT NSString *const AVAudioSessionCategoryRecord; /* Use this category when recording and playing back audio. */ AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord; /* Use this category when using a hardware codec or signal processor while not playing or recording audio. */ AVF_EXPORT NSString *const AVAudioSessionCategoryAudioProcessing; 啟用有了Category就可以啟動AudioSession了,啟動方法://AudioSession的啟動方法 extern OSStatus AudioSessionSetActive(Boolean active); extern OSStatus AudioSessionSetActiveWithFlags(Boolean active, UInt32 inFlags); //AVAudioSession的啟動方法 - (BOOL)setActive:(BOOL)active error:(NSError **)outError; - (BOOL)setActive:(BOOL)active withFlags:(NSInteger)flags error:(NSError **)outError NS_DEPRECATED_IOS(4_0, 6_0); - (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0); 啟動方法調用后必須要判斷是否啟動成功,啟動不成功的情況經常存在,例如一個前臺的app正在播放,你的app正在后臺想要啟動AudioSession那就會返回失敗。 一般情況下我們在啟動和停止AudioSession調用第一個方法就可以了。但如果你正在做一個即時語音通訊app的話(類似于微信、易信)就需要注意在deactive AudioSession的時候需要使用第二個方法,inFlags參數傳入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation(AVAudioSession給options參數傳入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)。當你的app deactive自己的AudioSession時系統會通知上一個被打斷播放app打斷結束(就是上面說到的打斷回調),如果你的app在deactive時傳入了NotifyOthersOnDeactivation參數,那么其他app在接到打斷結束回調時會多得到一個參數kAudioSessionInterruptionType_ShouldResume否則就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume),根據參數的值可以決定是否繼續播放。 大概流程是這樣的: 1. 一個音樂軟件A正在播放;2. 用戶打開你的軟件播放對話語音,AudioSession active;3. 音樂軟件A音樂被打斷并收到InterruptBegin事件;4. 對話語音播放結束,AudioSession deactive并且傳入NotifyOthersOnDeactivation參數;5. 音樂軟件A收到InterruptEnd事件,查看Resume參數,如果是ShouldResume控制音頻繼續播放,如果是ShouldNotResume就維持打斷狀態; 官方文檔中有一張很形象的圖來闡述這個現象:然而現在某些語音通訊軟件和某些音樂軟件卻無視了NotifyOthersOnDeactivation和ShouldResume的正確用法,導致我們經常接到這樣的用戶反饋:“你們的app在使用xx語音軟件聽了一段話后就不會繼續播放了,但xx音樂軟件可以繼續播放啊。” 好吧,上面只是吐槽一下。請無視我吧。 補充: 發現即使之前已經調用過AudioSessionInitialize方法,在某些情況下被打斷之后可能出現AudioSession失效的情況,需要再次調用AudioSessionInitialize方法來重新生成AudioSession。否則調用AudioSessionSetActive會返回560557673(其他AudioSession方法也雷同,所有方法調用前必須首先初始化AudioSession),轉換成string后為”!ini”即kAudioSessionNotInitialized,這個情況在iOS 5.1.x上尤其頻繁,iOS 7.x也偶有發生具體的原因還不知曉。 所以每次在調用AudioSessionSetActive時應該判斷一下錯誤碼,如果是上述的錯誤碼需要重新初始化一下AudioSession。 附上OSStatus轉成string的方法:
#import <Endian.h> NSString * OSStatusToString(OSStatus status) { size_t len = sizeof(UInt32); long addr = (unsigned long)&status; char cstring[5]; len = (status >> 24) == 0 ? len - 1 : len; len = (status >> 16) == 0 ? len - 1 : len; len = (status >> 8) == 0 ? len - 1 : len; len = (status >> 0) == 0 ? len - 1 : len; addr += (4 - len); status = EndianU32_NtoB(status); // strings are big endian strncpy(cstring, (char *)addr, len); cstring[len] = 0; return [NSString stringWithCString:(char *)cstring encoding:NSMacOSRomanStringEncoding]; } 打斷處理正常啟動AudioSession之后就可以播放音頻了,下面要講的是對于打斷的處理。之前我們說到打斷的回調在iOS 5下需要統一管理,在收到打斷開始和結束時需要發送自定義的通知。 使用AudioSession時打斷回調應該首先獲取kAudioSessionProperty_InterruptionType,然后發送一個自定義的通知并帶上對應的參數。static void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState) { AudioSessionInterruptionType interruptionType = kAudioSessionInterruptionType_ShouldNotResume; UInt32 interruptionTypeSize = sizeof(interruptionType); AudioSessionGetProperty(kAudioSessionProperty_InterruptionType, &interruptionTypeSize, &interruptionType); NSDictionary *userInfo = @{MyAudioInterruptionStateKey:@(inInterruptionState), MyAudioInterruptionTypeKey:@(interruptionType)}; [[NSNotificationCenter defaultCenter] postNotificationName:MyAudioInterruptionNotification object:nil userInfo:userInfo]; } 收到通知后的處理方法如下(注意ShouldResume參數):- (void)interruptionNotificationReceived:(NSNotification *)notification { UInt32 interruptionState = [notification.userInfo[MyAudioInterruptionStateKey] unsignedIntValue]; AudioSessionInterruptionType interruptionType = [notification.userInfo[MyAudioInterruptionTypeKey] unsignedIntValue]; [self handleAudioSessionInterruptionWithState:interruptionState type:interruptionType]; } - (void)handleAudioSessionInterruptionWithState:(UInt32)interruptionState type:(AudioSessionInterruptionType)interruptionType { if (interruptionState == kAudioSessionBeginInterruption) { //控制UI,暫停播放 } else if (interruptionState == kAudioSessionEndInterruption) { if (interruptionType == kAudioSessionInterruptionType_ShouldResume) { OSStatus status = AudioSessionSetActive(true); if (status == noErr) { //控制UI,繼續播放 } } } } 小結關于AudioSession的話題到此結束(碼字果然很累。。)。小結一下: * 如果最低版本支持iOS 5,可以使用AudioSession也可以考慮使用AVAudioSession,需要有一個類統一管理AudioSession的所有回調,在接到回調后發送對應的自定義通知;* 如果最低版本支持iOS 6及以上,請使用AVAudioSession,不用統一管理,接AVAudioSession的通知即可;* 根據app的應用場景合理選擇Category;* 在deactive時需要注意app的應用場景來合理的選擇是否使用NotifyOthersOnDeactivation參數;* 在處理InterruptEnd事件時需要注意ShouldResume的值。
新聞熱點
疑難解答