通过使用指定方式播放一段极小音频,比较播放的开始和完成时间,来判断当前静音按钮的状态。
我也针对常见的对音量方面的需求做了一个小工具,欢迎大家使用、指正。
2018年9月6日更新:
1.【修正】——App从后台切到前台时【AVPlayerItemDidPlayToEndTimeNotification】通知被无故调用的问题;
2.【修正】——在有其他音/视频播放时,初始化该工具会使正在播放的音/视频间断一下的问题。
解释下第一个bug:实际使用中发现,在使用原有AVPlayerItem的方式进行预播放音频后(用于本文最后说的那个小坑),使用AVPlayer正常播放音/视频,此时将App从后台切换到前台,用于预播放音频的AVPlayerItem会无故发出AVPlayerItemDidPlayToEndTimeNotification的通知,可能会影响到业务层。
为什么说是无故呢?因为App从后台切换到前台时,预播放音频早已经结束了很久,而且,当时结束时已经发出过AVPlayerItemDidPlayToEndTimeNotification通知了;也就是说,本已结束的音/视频,会由于从后台切到前台而反复发出AVPlayerItemDidPlayToEndTimeNotification通知(这也是很神奇,并没有想通是什么原理,也没有查到相关的资料,如有大神了解,希望您不吝赐教~)。
刚开始看到这个需求的时候,觉得这个应该会有相应的api,直接调用就可以了。但是实际一查才发现:并没有,准确的说是iOS5之后的版本相关api就不再支持了。细想下,其实这也挺符合Apple的行事作风的,app只要为用户提供服务就好了,用户的操作并不会让你知晓,极尽的保护用户的一切隐私。
那么在iOS5之后,我们在没有直接api的情况下,应该如何检测设备静音按钮处于什么状态呢?曲线救国~
首先鸣谢下RBDMuteSwitch这个库。因为我所使用核心方法也是借鉴了这个库中的方式来实现。
- 首先,这里我们要使用到一种平时不是很常用的播放音频的方式
-(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
方法支持的格式少,而且还要求音频时长为30s以下,但是,他有一个对我们最有用的特性:如果静音按钮为静音状态,那么会《立即》执行预先植入的soundCompletionBlock。相信到这您就可以瞬间想通后面的一切问题了~
针对这一特性,我通过记录开始播放和完成播放的时间,计算二者的差值,来判断静音按钮的状态。
由此也可以发现,这里需要以回调的方式来向询问者返回值,这里我选择了block,具体方式下文详述。而且为了回调足够及时,所以使用了一个长度仅为0.1s的音频(该音频素材也取自RBDMuteSwitch,再次鸣谢!)。
还有一点想强调一下,就是这里的通过静音按钮置于静音和将音量调小至0,是两种不同的状态,对于该种检测方式,只有通过静音按钮置于静音的方式,才会被判断为静音状态,完全满足我的要求。
- 那么其他的静音状态或者音量状态,怎么办呢?
根据我目前能够想象到的需求,我的这个工具主要提供了三个api:
- 判断当前静音按钮是否为静音状态
/**
用于回调当前静音按钮是否为静音状态的block
@param isMute 如果静音按钮当前为静音状态,则为YES,否则为NO
*/
typedef void(^MuteBlock)(BOOL isMute);
/**
当前静音按钮是否为静音状态
*/
@property (nonatomic, copy) MuteBlock muteBlock;
- 获取当前的真实音量(静音按钮处于静音状态时音量为0)
/**
用于回调当前真实音量的block
@param currentRealVolume 当前的真实音量
*/
typedef void(^RealVolumeBlock)(CGFloat currentRealVolume);
/**
当前的真实音量(静音按钮处于静音状态时音量为0)
*/
@property (nonatomic, copy) RealVolumeBlock realVolumeBlock;
- 监听音量变化(不考虑静音按钮处于静音状态的情况,该情况下仍正常返回实际的音量值,而不是0)
/**
用于监听音量变化的block
@param oldVolume 原始音量值
@param newVolume 新的音量值
*/
typedef void(^VolumeChangeBlock)(CGFloat oldVolume, CGFloat newVolume);
/**
监听音量变化(不考虑静音按钮处于静音状态的情况,该情况下仍正常返回实际的音量值,而不是0)
*/
@property (nonatomic, copy) VolumeChangeBlock volumeChangeBlock;
- 这里有一个小坑需要强调下:
常规的获取当前音量和监听音量变化的操作,相信大家都能瞬间实现,但是,如果你在执行这两个方法之前,没有过任何一次的音频播放,那么获取到的这两个值都是不准确的:
获取当前音量一直为一个值,也就是说即使你调整了音量,获取的还是最初的值;
对音量变化的监听则彻底不会触发-(void)observeValueForKeyPath:ofObject: change:context:
方法。
因此,我在工具的初始化方法中,执行了一次极小音频的播放,保证您在任何情况下获取的音量值及对音量变化的监听都是正确的。