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

在iOS多媒体开发的过程中,经常会用到视频播放器,简单是视频播放器,直接使用苹果封装好的MPMoviePlayerController和MPMoviePlayerViewController就可以实现视频播放功能了,但是,多数情况下,都需要自定制视频播放器,这是,就要使用神器AVPlayer来进行开发了,下面,就讲述一下AVPlayer的使用,这里列出两篇比较好的博客,供大家参考:

iOS开发系列--音频播放、录音、视频播放、拍照、视频录制

AVFoundation编程指南2-用AVPlayer播放视频
前一篇博客主要是简答介绍了怎样自定制视音频播放器,后一篇则比较深层次的讲解了视频播放器的相关信息,有兴趣的同学也可以了解一下AVFoundation框架,跟着本篇博客,读者可以自定义出一个完整的视频播放器。好了,废话不多说,开始进行视频播放的讲解。
首先,要使用AVPlayer进行自定制视频播放,要引入头文件:

#import 

因为AVPlayer属于AVFoundation框架,所以要引入这个头文件。其次,当然是要创建我们的视频播放器AVPlayer了,这里在.h文件中声明了一个全局的Player对象,便于在不同的函数中进行相关操作。

@property (nonatomic,strong) AVPlayer * player;

然后,在初始化方法中对其进行初始化。在初始化过程中,需要传入视频的URL,这个URL是NSURL类型的,这里简单说明一下,AVPlayer支持本地视频播放和媒体视频播放,因此,这个URL既可以是本地视频的URL,也可以是网络视频的URL,本篇博客选取了一段网络视频:

    //网络视频
    NSString * urlStr = [NSString stringWithFormat:@"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"];
    urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    NSURL * url = [NSURL URLWithString:urlStr];

首先用字符串传进来一个地址,然后,为了对字符串进行UTF-8编码,之前是用的其他的方法,但是,在iOS9之后,已经被启用了,所以这里用上面的方法,进行编码,前后,用编码之后的字符串初始化URL。其实,如果不考虑比较多的内容的话,直接用下面的方法就可以创建一个AVPlayer了:

self.player = [[AVPlayer alloc] initWithURL:url];

这其实是最简单的方法,但是一般不推荐使用,操作起来会很不方便。其实,自定制音频播放,也是用AVPlayer自定制,到这里的话,基本上就能够实现音频的播放了。但是,视频播放器的话,还要有画面,因此,还需要用这个Player去初始化一个图层,然后将图层加到当前的view的Layer上,这样,就有画面了,这里也是创建了一个全局的AVPlayerLayer对象:

@property (nonatomic,strong) AVPlayerLayer * playerLayer;

然后在.m中,接着上面的方法写入下面的代码:

    self.player = [[AVPlayer alloc] initWithURL:url];
    self.playerLayer.frame = self.layer.bounds;
    self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    [self.layer addSublayer:self.playerLayer];

我在创建的时候,是在一个View中创建的,所以直接是self.layer,如果是在Controller中,则是self.view.layer,然后,设置一下playerLayer的大小和方向。这样,就创建了一个Player。当然,上面说过,这只是简单的创建方式,通常情况下使用下面将要介绍的方式进行创建:
首先在.h中定义了一个AVPlayerItem对象:

@property (nonatomic,strong) AVPlayerItem * playerItem;

然后,在.m文件中进行下面的操作:

    AVURLAsset * movieAsset = [[AVURLAsset alloc] initWithURL:URL options:nil];
    self.playerItem = [AVPlayerItem playerItemWithAsset:movieAsset];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    self.playerLayer.frame = self.layer.bounds;
    self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    [self.layer addSublayer:self.playerLayer];



使用这种方式,就创建了一个player,并加到的当前视图的layer上。这里涉及到了其他的两个类AVURLAsset和AVPlayerItem。这里的AVURLAsset是AVAsset的子类,AVAsset不能直接用AVAsset进行初始化,需要用子类初始化,AVAsset和AVURLAsset其实是一个资源类,代表了视频资源,AVAsset也是AVFoundation中最终要的一个类,是对资源的抽象,想详细了解的,可以了解一下AVFoundation框架。而AVPlayerItem对应的其实就是要播放的视频了,先通过URL来创建一个视频播放器资源,然后,再用这个资源来初始化一个要播放的视频,之后,用这个视频初始化播放器,将播放器指定要播放的图层,基本上就创建完成了一个视频播放器,这里的初始化,是一层层进行的,希望大家不要被绕晕了,可以反复揣摩一下。player有一个rate属性,用来表示视频播放的速度,取值范围是0-1,1为正常速度,0的话,表示视频暂停了。上面说到,AVPlayerItem其实就相当于要播放的视频,因此,可以通过这个item,可以获得视频的总时长以及当前缓存到了哪里,所以,为了获取这些信息,要对item的相关属性进行监听:

    /**
     *  监听AVPlayerItem的属性
     */
    [self.playerItem addObserver:self forKeyPath:@"status" options:0 context:NULL];
    [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:0 context:NULL];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];


然后,添加监听的方法:

/**
 *  KVO监听playItem的属性变化
 *
 *  @param keyPath keyPath description
 *  @param object  object description
 *  @param change  change description
 *  @param context context description
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    AVPlayerItem * item = self.player.currentItem;
    if ([keyPath isEqualToString:@"status"]) {
        //正在播放
        if (AVPlayerItemStatusReadyToPlay == item.status) {
            NSLog(@"正在播放...,视频总长度:%.2f",CMTimeGetSeconds(item.duration));

        }
        else if (AVPlayerItemStatusUnknown == item.status){
            NSLog(@"视频加载中");
        }
        else if (AVPlayerStatusFailed == item.status){
            NSLog(@"视频获取失败");
            NSLog(@"%@",item.error);
        }
        
    } else if([keyPath isEqualToString:@"loadedTimeRanges"]){
        NSArray *array=item.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
        NSLog(@"共缓冲:%.2f",totalBuffer);
    }
}


通过监听status属性,来获得当前食品播放的状态,当状态为AVPlayerItemStatusReadyToPlay的时候,便是视频已经准备好了,此时,就可以播放当前的视频,可以在这里调用方法:

 [self.player play];

来播放当前的视频,player有一个属性叫做currentItem,这个属性,就是当前player的item,也就是前面初始化过程中的那个item。监听item的loadedTimeRanges属性,可以获得当前缓冲了多少视频以及视频的总长度。item的loadedTimeRanges其实是一个数组,里面存放了CMTimeRange类型的结构体,通过获得该array的firstObject可以获得本次缓冲的时间信息timeRange,timeRange.start表示本次缓冲的开始位置,timeRange.duration表示本次缓冲的视频长度,两者相加,就获得了缓冲的总时长,这就是好多播放器中,底部进度条中缓冲的视频长度的获取方式。由于loadedTimeRanges经常要变化,所以,会反复出发这个KVO的监听,因此,可以做到随时刷新缓冲进度。此外,当退出播放器页面的时候,要移除相关的观察者。

//移除观察者
-(void)removeObserverFromPlayerItem:(AVPlayerItem *)playerItem{
    [playerItem removeObserver:self forKeyPath:@"status"];
    [playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}

然后在dealloc调用这个方法该方法即可。
此外,当视频播放完成之后,还会有相关的通知,在这里,可以对其进行监听,当播放完成之后,进行相关的UI刷新:

/**
 *  添加播放器通知
 */
-(void)addNotification{
    //给AVPlayerItem添加播放完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
}

例如,可以更改播放按钮的图片:

- (void)playbackFinished:(NSNotification *)notification{
    [self.playButton setImage:[UIImage imageNamed:@"button_normal"] forState:UIControlStateNormal];
    
}

既然监听通知了,就要在dealloc中移除监听:

    [self removeNotification];

上面只是创建了视频播放器,下面将讲解视频播放器的控制。我们在进行视频自定制的时候,还要实现视频播放器的播放、暂停、继续播放、停止功能,因此,还要进行一些其他的操作,我这里将播放和继续播放进行了区分,这里说的播放,是从头开始播放,继续播放,则是从上次暂停的位置进行播放,因此要设置一个属性,来保存当前播放的位置:

//当前播放进度
@property (nonatomic,assign) double currentTime;

下面是从头播放的方法:

/**
 *  开始播放
 */
- (void)play{
    AVPlayerItem * item = self.player.currentItem;
//    [item seekToTime:CMTimeMakeWithSeconds(0, 1.0)];
    [item seekToTime:CMTimeMakeWithSeconds(0, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    self.progressBar.value = 0;
    [self.player play];
    //设置播放速度
}

在该方法中,用到了seekToTime方法,该方法用来从指定位置开始播放,传入一个CMTime类型的时间值,来指定比方的位置。我的项目中,添加了一个进度条(UISlider),来表示当前播放的进度,因此,当从0来说播放的时候,在这里将slider的value设置成0。这里还注释掉了一个seek方法,下面简单说一下,第一个seek方法,seek的时间没有第二个精确,但第二个更好性能,但还是推荐使用第二个。
下面是暂停的方法:

/**
 *  暂停播放
 */
- (void)pause{
    self.currentTime = [self playableCurrentTime];
    [self.player pause];
    //设置播放按钮
    [self.playButton setImage:[UIImage imageNamed:@"button_normal"] forState:UIControlStateNormal];
}

Avplayer自带一个pause方法,因此,这里可以直接调用,还有上面的play方法也是自带的。下面是resume方法:

/**
 *  继续播放
 */
- (void)resume{
    AVPlayerItem * item = self.player.currentItem;
//    [item seekToTime:CMTimeMakeWithSeconds(self.currentTime, 1.0)];
    [item seekToTime:CMTimeMakeWithSeconds(self.currentTime, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    [self.player play];
    //设置播放速度
    self.player.rate = self.rate;
}

上面说到了player的rate属性,其实这个属性的取值并不一定非要在0-1之间,当大于1的时候,会加速播放,小于1会减缓,这里可以通过这个属性来控制播放的速度。下面是停止方法:

/**
 *  停止
 */
- (void)stop{
    AVPlayerItem * item = self.player.currentItem;
//    [item seekToTime:CMTimeMakeWithSeconds(0, 1.0)];
    [item seekToTime:CMTimeMakeWithSeconds(0, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    [self.player pause];
    self.currentTime = 0;
    
}

这里使用seek方法,seek到0,然后调用player的pause方法,将视频pause,就实现了视频的stop功能。
此外,有些情况下,还会遇到这样的需求,就是将视频静音,在AVFoundation中,已经为我们提供了这样一个方法,就是将play的volume值设置为0。但是,这里要仔细考虑一下了,当设置为0之后,要想设置回来,怎么办呢?因此,要将当前的volume保存起来。定义一个变量保存当前的音量。

//音量
@property (nonatomic,assign) float volumn;

当点击静音按钮的时候,调用下面的方法即可:

/**
 *  设置静音
 *
 *  @param mute 静音传入的一个BOOL值,YES为静音,NO不静音
 */
- (void)playerMute:(BOOL)mute{
    if (mute) {
        [self.player setVolume:0];
    } else {
        [self.player setVolume:self.volumn];
    }
}

这里有一个问题需要注意,这里设置的音量,只是应用中的音量,并不是系统音量,也就是说,当系统音量为0的时候,及时这个volume再大,也是没有声音的,因此,当想要恢复音量的时候,这里的volume一般都设置1,即正常的音量。这时候,有些通过就会想了,那么,该怎么获取系统音量,然后点击手机上的音量键来调节音量,其实还是有方法的,下面是我写的另一篇博客:
iOS更改系统音量
想获取系统音量,请跳转到这个博客,了解一下就好,也可以查询一下其他的博客。
下面,再讲一些其他的控制。
自定制播放器的过程中,当视频播放进度发生改变的时候,我们也希望对应的进度条也跟着变化,因此,要监听视频播放的进度,可以使用下面的方法进行实现:

/**
 *  进度更新设置,监听视频播放进度,同时更新进度条的value
 */
- (void)addProgressBarObserver{
    AVPlayerItem *playerItem=self.player.currentItem;
    __weak typeof(self) weakSelf = self;
    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        float current = CMTimeGetSeconds(time);
        float total = CMTimeGetSeconds([playerItem duration]);
        if (current) {
            [weakSelf.progressBar setValue:(current/total) animated:YES];
        }
    }];
}

这里将进度条更新的操作封装成了一个方法,其实还是调用了AVPlayer自带的方法

- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

    这个方法的前一个参数还是CMTime类型的,后面是一个block,表示每个interval的时间,就回调一下这个block,这样的话,我们就可以在这里通过播放进度和总时长,来设置进度条的value值了。

    通过以上的方法,基本上就能实现一个简单的视频播放器了,可能有些地方说的不好或者说法有误,欢迎大家在下面进行评论,指出我的错误,大家共同进步。想进一步了解视频播放器的内容,欢迎阅读下一篇博客:

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

你可能感兴趣的:(iOS,AVPlayer,AVFoundation,iOS,AVAsset,AVPlayerItem)