天天看點

AudioSession詳解

前言

audiosession簡介

1. 确定你的app如何使用音頻(是播放?還是錄音?)

2. 為你的app選擇合适的輸入輸出裝置(比如輸入用的麥克風,輸出是耳機、手機功放或者airplay)

3. 協調你的app的音頻播放和系統以及其他app行為(例如有電話時需要打斷,電話結束時需要恢複,按下靜音按鈕時是否歌曲也要靜音等)

AudioSession詳解

audiosession

audiosession相關的類有兩個:

1. audiotoolbox中的audiosession

2. 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) 

    if (route) 

        cfrelease(route); 

    return hasheadset; 

+ (bool)isairplayactived 

    cfdictionaryref currentroutedescriptiondictionary = nil; 

    uint32 datasize = sizeof(currentroutedescriptiondictionary); 

    audiosessiongetproperty(kaudiosessionproperty_audioroutedescription, &datasize, &currentroutedescriptiondictionary); 

    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枚舉 

      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就維持打斷狀态;

AudioSession詳解

然而現在某些語音通訊軟體和某些音樂軟體卻無視了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的值。

繼續閱讀