用AVFoundation做一个视频播放器(一)

  本文主要是对视频播放器项目的介绍。本文主要实现了视频的播放和对播放情况的监听。

1. AVFoundation简介

  播放视频苹果提供了非常强大的AVFoundation框架,几乎可以满足我们所有的需求,播放短视频仅仅需要几行代码就可以搞定。

#import "ViewController.h"

// 导入AVFoundation框架
#import 

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 播放视频的链接
    NSString *strURL = @"https://www.apple.com/105/media/cn/home/2018/da585964_d062_4b1d_97d1_af34b440fe37/films/behind-the-mac/mac-behind-the-mac-tpl-cn_848x480.mp4";
    NSURL *url = [NSURL URLWithString:strURL];
     // 创建播放资源
    AVURLAsset *asset = [AVURLAsset assetWithURL:url];
    // 创建播放单元
    AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
    // 创建播放器
    AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
    // 播放视图
    AVPlayerLayer *avLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    avLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:avLayer];
   
    // 播放视频
    [player play];
}

@end

  下文主要是介绍一下视频相关类的作用以及常用接口。

1.1 AVURLAsset:播放资源

  它是AVFoundation的视频资源模型,提供媒体资源的不会随着视频播放变化的信息,例如视频的长度,格式等。虽然AVURLAsset是不可变的,但是它的属性却是异步加载的, 所以它的属性值并不是一直可用的,但是一旦可用了,值就不会再变了。它包含视频资源的音频、视频、字幕等。

1.2 AVPlayerItem:播放单元

  包含媒体资源的动态信息。是否可以播放,播放进度,缓存进度,视频的尺寸,是否播放完,缓冲情况(可以正常播放还是网络情况不好)等。

// 通过一个asset来实例化AVPlayerItem对象,相当于调用[AVPlayerItem playerItemWithAsset:_asset automaticallyLoadedAssetKeys:@[@"duration"]];
+ (instancetype)playerItemWithAsset:(AVAsset *)asset;
// 创建一个AVPlayerItem,将任意属性集委托给该框架,就可以自动载入对应的属性,省去了loadValuesAsynchronouslyForKeys: completionHandler载入需要访问其他资源属性。
+ (instancetype)playerItemWithAsset:(AVAsset *)asset automaticallyLoadedAssetKeys:(nullable NSArray *)automaticallyLoadedAssetKeys ;
// 当暂停的时候,是否可以继续使用网络资源继续缓冲。设置为NO,不可以,可以省电。
// ios9以后默认为NO,iOS9以前默认为YES
@property (nonatomic, assign) BOOL canUseNetworkResourcesForLiveStreamingWhilePaused;
// 设置播放器提前缓冲的时间,以防止播放中断。该属性定义了首选的前向缓冲区持续时间(秒)。如果设置为0,就不缓冲了,会经常卡顿。播放器将为大多数使用情况选择适当的缓冲级别。将此属性设置为较低值会增加播放停顿和重新缓冲的机会,而将其设置为较高值会增加对系统资源的需求;
@property (nonatomic) NSTimeInterval preferredForwardBufferDuration;
1.3 AVPlayer:播放器
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item;
// 播放
- (void)play;
// 暂停
- (void)pause;
// 播放速度,正常是1,小于1就是慢放,大于1就是快放
@property (nonatomic) float rate;
// 当前播放时间
- (CMTime)currentTime;
// iOS10之后的新属性,播放器是否应自动延迟播放以尽量减少停顿
// 设置为NO,解决在新系统下有时会播放不了的问题
@property (nonatomic) BOOL automaticallyWaitsToMinimizeStalling;
// 以下三个接口都是播放跳转
// toleranceBefore和toleranceAfter分别是允许之前和之后误差的时间
//  completionHandler 跳转之后的回调
//  调用 - (void)seekToTime:(CMTime)time;也是调用- (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter;,只不过toleranceBefore和toleranceAfter都是kCMTimeZero。
- (void)seekToTime:(CMTime)time;
- (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter;
- (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler NS_AVAILABLE(10_7, 5_0);

1.4 AVPlayerLayer:播放器界面
// 是视频适配AVPlayerLayer的方式
// 如果视频和AVPlayerLayer长宽比例不一致,就需要对视频做拉伸。
// 有三个值:AVLayerVideoGravityResizeAspect(视频的长宽比例保持不变拉伸,留空白);AVLayerVideoGravityResizeAspectFill(视频的长宽比例保持不变拉伸,铺满整个AVPlayerLayer,这样视频会有截掉一部分);AVLayerVideoGravityResize(改变视频的长宽比例,铺满整个AVPlayerLayer,这样视频会变形)
// 一般使用AVLayerVideoGravityResizeAspect
@property(copy) AVLayerVideoGravity videoGravity;
2.监听视频的播放情况

  以上的代码仅仅可以让我播放一个视频,除此之外我们还有很多需求。例如视频的长度,缓冲情况,播放情况等, 这就需要对AVPlayerItem进行KVO监听。

2.1AVPlayerItem的几个属性
1.视频资源加载的状态
@property (nonatomic, readonly) AVPlayerItemStatus status;

  这个属性有三个值:AVPlayerItemStatusUnknown(未知的)、
AVPlayerItemStatusReadyToPlay(准备好了,马上开始播放)、
AVPlayerItemStatusFailed (加载失败)。

//2.视频的尺寸
@property (nonatomic, readonly) CGSize presentationSize;
3. 缓冲的情况
@property (nonatomic, readonly) NSArray *loadedTimeRanges;

  这是一个数组,里面的元素是CMTimeRange结构体,它表示视频缓冲到哪里了

// 获取缓存的进度
- (NSTimeInterval)loadedTime {
    
    NSArray *timeRanges = _playerItem.loadedTimeRanges;
    // 播放的进度
    CMTime currentTime = _player.currentTime;
    
    // 判断播放的进度是否在缓存的进度内
    BOOL included = NO;
    CMTimeRange firstTimeRange = {0};
    if (timeRanges.count > 0) 
    {
        firstTimeRange = [[timeRanges objectAtIndex:0] CMTimeRangeValue];
        if (CMTimeRangeContainsTime(firstTimeRange, currentTime)) {
            included = YES;
        }
    }
    
    // 存在返回缓存的进度
    if (included) {
        CMTime endTime = CMTimeRangeGetEnd(firstTimeRange);
        NSTimeInterval loadedTime = CMTimeGetSeconds(endTime);
        if (loadedTime > 0) {
            return loadedTime;
        }
    }
    return 0;
}
4. 视频是否可以正常播放
@property (nonatomic, readonly, getter=isPlaybackLikelyToKeepUp) BOOL playbackLikelyToKeepUp;

  这这个属性是对视频是否可以继续播放的一种预测,如果为NO,视频就会暂停。视频不能继续播放的原因主要有两个,视频没有缓冲了和缓存的数据不能正确解码(视频播放器不支持视频的格式)。所以当playbackBufferEmpty为NO,playbackBufferFull(是否已经全部缓存)为YES时,playbackLikelyToKeepUp也有可能为NO。

5.缓冲是否为空

@property (nonatomic, readonly, getter=isPlaybackBufferEmpty) BOOL playbackBufferEmpty;

这个值为YES,视频就会暂停。当这个值为NO,视频也可能不能继续播放。具体原因参考上面的属性。

2.2 监听视频的播放情况
- (void)addObserver
{
    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [self.playerItem addObserver:self forKeyPath:@"presentationSize" options:NSKeyValueObservingOptionNew context:nil];
    [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
    [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    
    // 表示0.5s
    CMTime interval = CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC);
   __weak typeof(self) weakSelf = self;
    // 增加播放进度的监听 每0.5秒调用一次
    _timeObserver = [self.player addPeriodicTimeObserverForInterval:interval queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        if (!weakSelf) return;
        NSArray *loadedRanges = weakSelf.playerItem.seekableTimeRanges;
        if (loadedRanges.count > 0 && weakSelf.playerItem.duration.timescale != 0) {
            NSLog(@"播放进度 = %.2f",CMTimeGetSeconds(time));
            NSLog(@"视频总时长 = %.2f",CMTimeGetSeconds(weakSelf.playerItem.duration));
        }
    }];
    
    // 增加播放结束的监听
    _itmePlaybackEndObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"本视频播放结束了");
    }];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    
    if ([keyPath isEqualToString:@"status"]) {
        switch (self.playerItem.status) {
            case AVPlayerItemStatusUnknown:
                NSLog(@"未知的播放状态");
                [self.player play];
                break;
            case AVPlayerItemStatusReadyToPlay:
                NSLog(@"马上可以播放了");
                break;
            case AVPlayerItemStatusFailed:
                NSLog(@"发生错误:%@",self.player.error);
                break;
            default:
                break;
        }
    }
    
    if ([keyPath isEqualToString:@"presentationSize"]) {
        NSLog(@"视频的尺寸:%@",NSStringFromCGSize(self.playerItem.presentationSize));
    }
    
    if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        NSLog(@"缓冲进度:%.2f",[self loadedTime]);

    }
    
    if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
        NSLog(@"%@可以正常播放",self.playerItem.playbackLikelyToKeepUp ? @"" : @"不");
    }
    
    if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
        NSLog(@"%@有缓冲",self.playerItem.playbackBufferEmpty ? @"没": @"");
    }
}

// 移除观察者,否则会内存泄漏 
- (void)removeObserver
{
    @try{
        [self.playerItem removeObserver:self forKeyPath:@"status"];
        [self.playerItem removeObserver:self forKeyPath:@"presentationSize"];
        [self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
        [self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
        [self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
    } @catch(NSException *e){
        NSLog(@"failed to remove observer");
    }
}

// 获取缓存的进度
- (NSTimeInterval)loadedTime {
    
    NSArray *timeRanges = _playerItem.loadedTimeRanges;
    // 播放的进度
    CMTime currentTime = _player.currentTime;
    
    // 判断播放的进度是否在缓存的进度内
    BOOL included = NO;
    CMTimeRange firstTimeRange = {0};
    if (timeRanges.count > 0) {
        firstTimeRange = [[timeRanges objectAtIndex:0] CMTimeRangeValue];
        if (CMTimeRangeContainsTime(firstTimeRange, currentTime)) {
            included = YES;
        }
    }
    
    // 存在返回缓存的进度
    if (included) {
        CMTime endTime = CMTimeRangeGetEnd(firstTimeRange);
        NSTimeInterval loadedTime = CMTimeGetSeconds(endTime);
        if (loadedTime > 0) {
            return loadedTime;
        }
    }
    return 0;
}

@end

  通过KVO、通知和系统提供的方法,可以完美监测播放的缓冲情况、播放进度、播放结束等,这样我们就可以给视频增加播放进度条、缓冲进度条等UI,可以对播放情况不好时做一些处理。
  当self.playerItem.status = AVPlayerItemStatusReadyToPlay的时候,我们要再执行一次[self play];。因为当self.playerItem.playbackLikelyToKeepUp为NO的时候会暂停播放,为YES的时候确不会自动播放。如果我们不执行,即使网络好了,视频也不会继续播放了。
  当没有缓冲的时候,就要暂停,因为如果还继续播放,就会卡顿,还可能没有声音,所以我们就要缓冲一会。

// 当网络不好的时候,会多次调用这里,
- (void)buffingSomeSeconds
{
    // 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来
    [self.player pause];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 如果此时用户已经暂停了,则不再需要开启播放了
        if (!self.playing) {return;}
        
        [self play];
        // 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间
        if (!self.playerItem.isPlaybackLikelyToKeepUp)
        {
            [self buffingSomeSeconds];
        }
    });
}

  关于视频播放器,还有很多其他的注意事项,下文会慢慢介绍。

你可能感兴趣的:(用AVFoundation做一个视频播放器(一))