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

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

1. AVFoundation简介

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

#import "ViewController.h"

// 导入AVFoundation框架
#import <AVFoundation/AVFoundation.h>

@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<NSString *> *)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<NSValue *> *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<NSKeyValueChangeKey,id> *)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];
        }
    });
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容