通過(guò)使用指定方式播放一段極小音頻,比較播放的開始和完成時(shí)間,來(lái)判斷當(dāng)前靜音按鈕的狀態(tài)。
我也針對(duì)常見的對(duì)音量方面的需求做了一個(gè)小工具,歡迎大家使用、指正。
2018年9月6日更新:
1.【修正】——App從后臺(tái)切到前臺(tái)時(shí)【AVPlayerItemDidPlayToEndTimeNotification】通知被無(wú)故調(diào)用的問(wèn)題;
2.【修正】——在有其他音/視頻播放時(shí),初始化該工具會(huì)使正在播放的音/視頻間斷一下的問(wèn)題。
解釋下第一個(gè)bug:實(shí)際使用中發(fā)現(xiàn),在使用原有AVPlayerItem的方式進(jìn)行預(yù)播放音頻后(用于本文最后說(shuō)的那個(gè)小坑),使用AVPlayer正常播放音/視頻,此時(shí)將App從后臺(tái)切換到前臺(tái),用于預(yù)播放音頻的AVPlayerItem會(huì)無(wú)故發(fā)出AVPlayerItemDidPlayToEndTimeNotification的通知,可能會(huì)影響到業(yè)務(wù)層。
為什么說(shuō)是無(wú)故呢?因?yàn)锳pp從后臺(tái)切換到前臺(tái)時(shí),預(yù)播放音頻早已經(jīng)結(jié)束了很久,而且,當(dāng)時(shí)結(jié)束時(shí)已經(jīng)發(fā)出過(guò)AVPlayerItemDidPlayToEndTimeNotification通知了;也就是說(shuō),本已結(jié)束的音/視頻,會(huì)由于從后臺(tái)切到前臺(tái)而反復(fù)發(fā)出AVPlayerItemDidPlayToEndTimeNotification通知(這也是很神奇,并沒(méi)有想通是什么原理,也沒(méi)有查到相關(guān)的資料,如有大神了解,希望您不吝賜教~)。
剛開始看到這個(gè)需求的時(shí)候,覺(jué)得這個(gè)應(yīng)該會(huì)有相應(yīng)的api,直接調(diào)用就可以了。但是實(shí)際一查才發(fā)現(xiàn):并沒(méi)有,準(zhǔn)確的說(shuō)是iOS5之后的版本相關(guān)api就不再支持了。細(xì)想下,其實(shí)這也挺符合Apple的行事作風(fēng)的,app只要為用戶提供服務(wù)就好了,用戶的操作并不會(huì)讓你知曉,極盡的保護(hù)用戶的一切隱私。
那么在iOS5之后,我們?cè)跊](méi)有直接api的情況下,應(yīng)該如何檢測(cè)設(shè)備靜音按鈕處于什么狀態(tài)呢?曲線救國(guó)~
首先鳴謝下RBDMuteSwitch這個(gè)庫(kù)。因?yàn)槲宜褂煤诵姆椒ㄒ彩墙梃b了這個(gè)庫(kù)中的方式來(lái)實(shí)現(xiàn)。
- 首先,這里我們要使用到一種平時(shí)不是很常用的播放音頻的方式
-(void)monitorMute{
CFURLRef soundFileURLRef = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("detection"), CFSTR("aiff"), NULL);
SystemSoundID soundFileID;
AudioServicesCreateSystemSoundID(soundFileURLRef, &soundFileID);
AudioServicesAddSystemSoundCompletion(soundFileID, NULL, NULL, soundCompletionBlock, (__bridge void*) self);
AudioServicesPlaySystemSound(soundFileID);
}
static void soundCompletionBlock(SystemSoundID SSID, void *mySelf){
AudioServicesRemoveSystemSoundCompletion(SSID);
[[XTVolumeMonitor defaultMonitor] playToEnd];
}
AudioServicesPlaySystemSound方法支持的格式少,而且還要求音頻時(shí)長(zhǎng)為30s以下,但是,他有一個(gè)對(duì)我們最有用的特性:如果靜音按鈕為靜音狀態(tài),那么會(huì)《立即》執(zhí)行預(yù)先植入的soundCompletionBlock。相信到這您就可以瞬間想通后面的一切問(wèn)題了~
針對(duì)這一特性,我通過(guò)記錄開始播放和完成播放的時(shí)間,計(jì)算二者的差值,來(lái)判斷靜音按鈕的狀態(tài)。
由此也可以發(fā)現(xiàn),這里需要以回調(diào)的方式來(lái)向詢問(wèn)者返回值,這里我選擇了block,具體方式下文詳述。而且為了回調(diào)足夠及時(shí),所以使用了一個(gè)長(zhǎng)度僅為0.1s的音頻(該音頻素材也取自RBDMuteSwitch,再次鳴謝?。?/p>
還有一點(diǎn)想強(qiáng)調(diào)一下,就是這里的通過(guò)靜音按鈕置于靜音和將音量調(diào)小至0,是兩種不同的狀態(tài),對(duì)于該種檢測(cè)方式,只有通過(guò)靜音按鈕置于靜音的方式,才會(huì)被判斷為靜音狀態(tài),完全滿足我的要求。
- 那么其他的靜音狀態(tài)或者音量狀態(tài),怎么辦呢?
根據(jù)我目前能夠想象到的需求,我的這個(gè)工具主要提供了三個(gè)api:
- 判斷當(dāng)前靜音按鈕是否為靜音狀態(tài)
/**
用于回調(diào)當(dāng)前靜音按鈕是否為靜音狀態(tài)的block
@param isMute 如果靜音按鈕當(dāng)前為靜音狀態(tài),則為YES,否則為NO
*/
typedef void(^MuteBlock)(BOOL isMute);
/**
當(dāng)前靜音按鈕是否為靜音狀態(tài)
*/
@property (nonatomic, copy) MuteBlock muteBlock;
- 獲取當(dāng)前的真實(shí)音量(靜音按鈕處于靜音狀態(tài)時(shí)音量為0)
/**
用于回調(diào)當(dāng)前真實(shí)音量的block
@param currentRealVolume 當(dāng)前的真實(shí)音量
*/
typedef void(^RealVolumeBlock)(CGFloat currentRealVolume);
/**
當(dāng)前的真實(shí)音量(靜音按鈕處于靜音狀態(tài)時(shí)音量為0)
*/
@property (nonatomic, copy) RealVolumeBlock realVolumeBlock;
- 監(jiān)聽音量變化(不考慮靜音按鈕處于靜音狀態(tài)的情況,該情況下仍正常返回實(shí)際的音量值,而不是0)
/**
用于監(jiān)聽音量變化的block
@param oldVolume 原始音量值
@param newVolume 新的音量值
*/
typedef void(^VolumeChangeBlock)(CGFloat oldVolume, CGFloat newVolume);
/**
監(jiān)聽音量變化(不考慮靜音按鈕處于靜音狀態(tài)的情況,該情況下仍正常返回實(shí)際的音量值,而不是0)
*/
@property (nonatomic, copy) VolumeChangeBlock volumeChangeBlock;
- 這里有一個(gè)小坑需要強(qiáng)調(diào)下:
常規(guī)的獲取當(dāng)前音量和監(jiān)聽音量變化的操作,相信大家都能瞬間實(shí)現(xiàn),但是,如果你在執(zhí)行這兩個(gè)方法之前,沒(méi)有過(guò)任何一次的音頻播放,那么獲取到的這兩個(gè)值都是不準(zhǔn)確的:
獲取當(dāng)前音量一直為一個(gè)值,也就是說(shuō)即使你調(diào)整了音量,獲取的還是最初的值;
對(duì)音量變化的監(jiān)聽則徹底不會(huì)觸發(fā)-(void)observeValueForKeyPath:ofObject: change:context:方法。
因此,我在工具的初始化方法中,執(zhí)行了一次極小音頻的播放,保證您在任何情況下獲取的音量值及對(duì)音量變化的監(jiān)聽都是正確的。