AVPlayer自定制视频播放器(2)——耳机线控、中断以及AVAudioSession的使用

在上一篇博客中说到了使用AVPlayer进行自定义视频播放器。这里讲继续讲述视频播放器的自定制。下面是上一篇博客的链接,本篇博客将承接上一篇博客进行讲解,如果有AVPlayer自定制视频播放器基础的同学,可以不必看上一篇博客,直接进入这篇。


AVPlayer自定义视频播放器(1)——视频播放器基本实现


相信你已经会使用AVPlayer进行视频播放器的自定制,并且,能够进行基本的开始、暂停、静音、快放等一些基本操作,这里主要讲解一些特殊的操作。主要讲解耳机线控、电话呼入中断和应用退到后台等操作。

首先将一个简单的电话呼入操作吧。其实,当有电话呼入的时候,系统会自动发送一个中断的通知给当前运行的各个应用,因此,在这里只要注册一下这个通知,然后在对应的方法中,对中断进行相关的处理,就可以做到暂停视音频的播放了。


    /**
     *  注册中断通知
     */
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];



这里涉及到了一个AVAudioSessionInterruptionNotification,这个其实是视音频会话被打断的通知,AVAudioSession其实是视频、音频以及录音功能通用的一个会话,不要根据它的名字中写着Audio就以为只是音频的会话,这个其实是通用的。addObserver当然就是指定当前的页面为监听对象,我在项目中将Player放在了一个view,所以,这里指的是这个view对象。selector当然就是通知的回调方法。后面的object是要传入到回调方法中的参数,这里一定要将这个AVAudioSession传入进入,目的是在回调中获得session对象,然后从session中获得响应的中断信息,然后根据终端信息,进行响应的操作。回调函数代码如下:


/**
 *  中断处理函数
 *
 *  @param notification 通知对象
 */
- (void)handleInterruption:(NSNotification *)notification{
    NSDictionary * info = notification.userInfo;
    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    //中断开始和中断结束
    if (type == AVAudioSessionInterruptionTypeBegan) {
        //当被电话等中断的时候,调用这个方法,停止播放
        [self pause];
        if (self.delegate) {
            [self.delegate playbackStopped];
        }
    } else {
        /**
         *  中断结束,userinfo中会有一个InterruptionOption属性,
         *  该属性如果为resume,则可以继续播放功能
         */
        AVAudioSessionInterruptionOptions option = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
        if (option == AVAudioSessionInterruptionOptionShouldResume) {
            [self resume];
            [self.delegate playbackBegin];
        }
    }
}


这里要对这个方法进行详细的讲解。这里引用《AV Foundation开发秘籍》中的部分内容进行讲解。书中的内容将用红色字体标出,以表示对作者版权的尊重。推送的通知中包含一个带有许多重要信息的userInfo字典根据这个字典可以确定采取哪些合适的操作。在handleInterruption:方法中,首先通过检索AVAudioSessionInterruptionTypeKey的值确定中断类型(type)。返回值是AVAudioSessionInterruptionType,这是用于表示中断开始或结束的么及类型。

typedef NS_ENUM(NSUInteger, AVAudioSessionInterruptionType)
{
	AVAudioSessionInterruptionTypeBegan = 1,  /* the system has interrupted your audio session */
	AVAudioSessionInterruptionTypeEnded = 0,  /* the interruption has ended */
} NS_AVAILABLE_IOS(6_0);

上面就是这个枚举类型,也就是上面的handleInterruption:方法中的那个if语句。上面的代码表示,当中断开始的时候,也就是当type ==AVAudioSessionInterruptionTypeBegan时,暂停当前的视音频播放,也就是上面的[self pause]方法,该方法写在了上一篇博客中。如果中断类型为AVAudioSessionInterruptionTypeEnded,userInfo字典会包含一个AVAudioSessionInterruptionOption值,来表示音频会话是否已经重新激活以及是否可以再次播放,其实这也是一个枚举类型:


/* For use with AVAudioSessionInterruptionNotification */
typedef NS_OPTIONS(NSUInteger, AVAudioSessionInterruptionOptions)
{
	AVAudioSessionInterruptionOptionShouldResume = 1
} NS_AVAILABLE_IOS(6_0);

细心地读者会发现,我在上面的方法中,不管是began还是ended中,都有代理方法:


        if (self.delegate) {
            [self.delegate playbackStopped];
        }

        if (self.delegate) {
            [self.delegate playbackBegin];
        }

这个代理主要是方便父视图或者是controller进行相关的UI操作,协议定义如下:


//视频播放中断的代理以及相应的方法,controller刷新UI的方法写在这里
@protocol PlayerViewDelegate 
//中断方法
- (void)playbackStopped;
//重新开始播放方法
- (void)playbackBegin;

应用程序的视图控制器已经采用该协议,并将其设置为委托。这提供了一种简单的方法来更新应用程序的用户界面。其实,当视频中断开始或者中断结束继续播放的时候,也可以发送通知,在controller中注册通知,监听状态改变。但笔者参考了部分书籍和其他的一些视频播放器,都使用了代理的方式,所以这里推荐使用代理方式,来实现回调刷新UI的功能。

此外,还要做出对路线改变的响应。所谓路线改变,就是插上耳机、拔出耳机,因为在使用视频播放器的过程中,肯定会涉及到耳机的使用,因此,必须要对这种情况进行处理,保证应用程序对线路变换做出正确的响应。在iOS设备上添或移除音频输入、输出线路时,会发生线路改变。有多重原因导致线路的变化,比如用户插入耳机或者断开USB麦克风。当这些事件发生时,,音频会根据情况改变输入或者输出线路,同时,AVAudioSession会广播一个描述该改变的通知给所有的侦听器,为遵循Apple的Human Interface Guidelines(HIG)的相关定义,应用程序应该成为这些相关侦听器中的一员。

正常情况下,当我们点击开始播放视频时,并在播放期间插入耳机,音频输出路线变为耳机插孔并继续正常播放,这正是我们所期望的效果。保持音频处于播放状态,断开耳机连接,音频路线再次回到设备的内置扬声器,我们再次听到了声音。虽然路线变化通预期的一样,不过,按照苹果公司的相关文档,该音频应该处于静音状态,当用户插入耳机时,隐含的意思是用户不希望外界听到具体的音频内容,这就意味着当用户断开耳机时,播放的内容可能需要继续保密,所以,我们需要停止音频播放。

从上面说的内容可以知道,当拔出耳机,一定要停止音频播放,所以,一定要对相应的状态进行处理。看了上面的代码,相比很快就会想到,在这里也是需要注册AVAudioSession的发送的通知,这里用到的通知是AVAudioSessionRouteChangeNotification,和前面一样,也是从userInfo字典中取出相关的参数,通过判断参数来进行相应的处理。注册通知的方法如下:


    //添加耳机状态监听
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionRouteChangeNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];


前面的方法是先移除这个通知,后面是添加通知,有些资料中的写法是先移除可能存在的已经注册的通知,然后重新注册通知,其实也可以不写前面的那段代码,这部分,有个object对象,为nil也是可以的,因为可以通过单例来访问AVAudioSession对象。然后,就是对通知进行相关的处理,方法如下:

/**
 *  音频输出改变触发事件
 *
 *  @param notification 通知
 */
- (void)routeChange:(NSNotification *)notification{
    NSDictionary *dic = notification.userInfo;
    int changeReason= [dic[AVAudioSessionRouteChangeReasonKey] intValue];
    //等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示旧输出不可用
    if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];
        //原设备为耳机则暂停
        if ([portDescription.portType isEqualToString:@"Headphones"]) {
            UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
            AVAudioSession * session = [AVAudioSession sharedInstance];
            [session setPreferredIOBufferDuration:audioRouteOverride error:nil];
            //如果视频正在播放,会自动暂停,这里用来设置按钮图标
            if (self.playerState == playerViewPlaystatePlaying) {
                [self pause];
                [self.delegate playbackStopped];
            }
            
        }
        
    }
}

从userInfo中取出AVAudioSessionRouteChangeReasonKey的value值,并转成int类型,赋值changeReason变量,其实获取到的数据是一个枚举类型的,该枚举类型保存在AVAudioSession.h中,

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
} NS_AVAILABLE_IOS(6_0);

然后对枚举类型进行判断,如果为AVAudioSessionRouteChangeReasonOldDeviceUnavailable,则表示旧设备不可用,也就是插入耳机后,外放不可用,或者拔出耳机后,耳机不可用,然后定义一个AVAudioSessionRouteDescription类型的变量,该变量表示的是播放的路线描述信息,这里取出路线之前所使用设备的路线描述信息,即dic[AVAudioSessionRouteChangePreviousRouteKey]。获取了路线描述信息后,还要根据路线描述信息,获取对应的输出端口描述信息,也就是AVAudioSessionPortDescription *portDescription = [routeDescription.outputsfirstObject];然后从端口的描述信息中取出端口的类型,也就是portDescription.portType,这个类型其实是一个字符串类型,可以对这个类型进行判断,如果为“HeadPhones”,则表示为耳机,这儿时候,表示旧设备的类型为耳机,此时是拔出了耳机,因此,要暂停当前的视音频播放。同时,要强行将AVAudioSession的输出设备设置成为speaker,也就是手机底部的外放,因为手机的音频播放有外放,还有打电话的那个声音输出口以及耳机,所以,要设置成speaker。到这里,基本上就完成了一个视音频播放器的自定制。

  其实,在视频播放器创建的时候,最好还是在初始化的过程中,对AVAudioSession进行播放端口的设置,以防其他页面的视音频播放器对AVAudioSession进行了更改,造成音量播放问题。


//设置session,防止播放时没有声音,自动识别当前播放模式,是耳机还是外放
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:NULL];
    UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
    [[AVAudioSession sharedInstance] setPreferredIOBufferDuration:audioRouteOverride error:nil];

这里的第一个方法中的SetCategory传入的参数是AVAudioSessionCategoryPlayAndRecord,表示应用同时支持视音频播放和录音,这样,能够防止添加录音功能后,播放模式就不是speaker了,而变成了顶部打电话的那个播放器了(忘记叫什么名字了)。 后面设置的那个withOption,就是表示,默认情况下,音量播放是通过speaker进行播放的。如果在应用中,还有录音功能,当拔掉耳机后,即使不录音,视频播放也不会是speaker,即使前面硬改,还是没法实现speaker,因此,这里设置一下,就不会有问题了。下面两行代码,是设置播放模式为speaker,虽然这么设置,但是,如果打开视频前,就已经插入耳机了,仍然是耳机播放,不是外放。所以不必担心播放前插入耳机,造成声音外放。

写到这里,包括上一篇博客,基本上已经实现了一个完整的视频播放器了,而且已经将平时开发过程中能够遇到的问题都已经考虑进来了,感谢耐心读者花费这么长时间看完。如果博客中有什么错误的部分,希望大家批评指正,互相学习。

  上一篇博客地址:AVPlayer自定制视频播放器(1)——视频播放器基本实现


你可能感兴趣的:(iOS)