AVPlayer详解系列(一)参数设置

作者: zhangferry | 来源:发表于2017-08-25 15:57 被阅读4661次

    最近工作内容基本都是围绕视频播放展开的,从AVPlayer到IJKPlayer,期间遇到挺多问题,趟了很多bug,也总结了一些心得。对AVPlayer了解的更多一些,因为涉及点比较多,所以打算做一个系列详尽的写一下这部分内容。希望大家多多支持,有问题的地方欢迎指正。

    思维导图

    先来一张思维导图,作为这篇文章的目录索引:


    AVPlayer.png

    为什么使用AVPlayer:

    首先在iOS平台使用播放视频,可用的选项一般有这四个,他们各自的作用和功能如下:

    使用环境 优点 缺点
    MPMoviePlayerController MediaPlayer 简单易用 不可定制
    AVPlayerViewController AVKit 简单易用 不可定制
    AVPlayer AVFoundation 可定制度高,功能强大 不支持流媒体
    IJKPlayer IJKMediaFramework 定制度高,支持流媒体播放 使用稍复杂

    由此可以看出,如果我们不做直播功能AVPlayer就是一个最优的选择。

    另外AVPlayer是一个可以播放任何格式的全功能影音播放器
    支持视频格式: WMV,AVI,MKV,RMVB,RM,XVID,MP4,3GP,MPG等。
    支持音频格式:MP3,WMA,RM,ACC,OGG,APE,FLAC,FLV等。
    所以不得不说苹果亲儿子还是很强大的,是做视频开发的首选。

    如何使用

    AVPlayer存在于AVFoundation框架,我们使用时需要导入:
    #import <AVFoundation/AVFoundation.h>

    几个播放相关的参数

    在创建一个播放器之前我们需要先了解一些播放器相关的类

    • AVPlayer:控制播放器的播放,暂停,播放速度
    • AVURLAsset : AVAsset 的一个子类,使用 URL 进行实例化,实例化对象包换 URL 对应视频资源的所有信息。
    • AVPlayerItem:管理资源对象,提供播放数据源
    • AVPlayerLayer:负责显示视频,如果没有添加该类,只有声音没有画面

    我们这片文章就围绕这几个参数展开,光说这些你可能还有点不明白,那我们就围绕一个最简单的播放器做起,一点点扩展功能,在具体讲解这几个参数的作用。

    最简单的播放器

    根据上面描述,我们知道AVPlayer是播放的必要条件,所以我们可以构建的极简播放器就是:

    NSURL *playUrl = [NSURL URLWithString:@"http://baobab.wdjcdn.com/14573563182394.mp4"];
    self.player = [[AVPlayer alloc] initWithURL:playUrl];
    [self.player play];
    

    是不是很简单,只有三行代码!
    但是它太简单了,仅可以完成音频的播放,连画面都没有。回看上面播放相关类的介绍,是因为缺少AVPlayerLayer;作为一个播放器,我不能只播放一条视频啊,我还想根据需要切换视频,那我们就得把AVPlayerItem也加上。
    加上这两个属性之后的播放器是这样的:

    NSURL *playUrl = [NSURL URLWithString:@"http://baobab.wdjcdn.com/14573563182394.mp4"];
    self.playerItem = [AVPlayerItem playerItemWithURL:playUrl];
    //如果要切换视频需要调AVPlayer的replaceCurrentItemWithPlayerItem:方法
    self.player = [AVPlayer playerWithPlayerItem:_playerItem];
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    self.playerLayer.frame = _videoView.bounds;
    //放置播放器的视图
    [self.videoView.layer addSublayer:self.playerLayer];
    [_player play];
    

    现在的播放器稍微完整了一些,我们在自己创建的容器里可以看到画面了!

    更多功能

    但是它作为一个视频播放器,还是有很多不能让人满意的地方。例如:没有暂停、快进快退、倍速播放等,另外如果遇到url错误是不是还要有播放失败的提示,还有播放完成的相关提示。
    为完成这些,我们需要对AVPlayerItemAVPlayerLayer进一步了解一下。

    一、AVPlayer的控制

    前面讲过该类是控制视频播放行为的,他的使用比较简单。
    播放视频:

    [self.player play];
    

    暂停视频:

    [self.player pause];
    

    更改速度:

    self.player.rate = 1.5;//注意更改播放速度要在视频开始播放之后才会生效
    

    还有一下其他的控制,我们可以调转到系统API进行查看

    二、AVPlayerItem的控制

    AVPlayerItem作为资源管理对象,它控制着视频从创建到销毁的诸多状态。

    1、播放状态 status

    typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
        AVPlayerItemStatusUnknown,//未知
        AVPlayerItemStatusReadyToPlay,//准备播放
        AVPlayerItemStatusFailed//播放失败
    };
    

    我们使用KVO监测playItem.status,可以获取播放状态的变化

    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    

    在监听回调中:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if ([object isKindOfClass:[AVPlayerItem class]]) {
        if ([keyPath isEqualToString:@"status"]) {
            switch (_playerItem.status) {
                case AVPlayerItemStatusReadyToPlay:
                    //推荐将视频播放放在这里
                    [self play];
                    break;
                    
                case AVPlayerItemStatusUnknown:
                    NSLog(@"AVPlayerItemStatusUnknown");
                    break;
                    
                case AVPlayerItemStatusFailed:
                    NSLog(@"AVPlayerItemStatusFailed")
                    break;
                    
                default:
                    break;
            }
            
        }
    }
    

    虽然设置完播放配置我们可以直接调用[self.player play];进行播放,但是更稳妥的方法是在回调收到AVPlayerItemStatusReadyToPlay时进行播放

    2、视频的时间信息

    在AVPlayer中时间的表示有一个专门的结构体CMTime

    typedef struct{
        CMTimeValue    value;     // 帧数
        CMTimeScale    timescale;  // 帧率(影片每秒有几帧)
        CMTimeFlags    flags;        
        CMTimeEpoch    epoch;    
    } CMTime;
    

    CMTime是以分数的形式表示时间,value表示分子,timescale表示分母,flags是位掩码,表示时间的指定状态。

    获取当前播放时间,可以用value/timescale的方式:

    float currentTime = self.playItem.currentTime.value/item.currentTime.timescale;
    

    还有一种利用系统提供的方法,我们用它获取视频总时间:

    float totalTime   = CMTimeGetSeconds(item.duration);
    

    如果我们想要添加一个计时的标签不断更新当前的播放进度,有一个系统的方法:

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

    方法名如其意, “添加周期时间观察者” ,参数1 interal 为CMTime 类型的,参数2 queue为串行队列,如果传入NULL就是默认主线程,参数3 为CMTime 的block类型。
    简而言之就是,每隔一段时间后执行 block。
    比如:我们把interval设置成CMTimeMake(1, 10),在block里面刷新label,就是一秒钟刷新10次。

    正常观察播放进度一秒钟一次就行了,所以可以这么写:

    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:nil usingBlock:^(CMTime time) {
        
        AVPlayerItem *item = WeakSelf.playerItem;
        NSInteger currentTime = item.currentTime.value/item.currentTime.timescale;
        NSLog(@"当前播放时间:%ld",currentTime);
    }];
    

    3、loadedTimeRange 缓存时间

    获取视频的缓存情况我们需要监听playerItem的loadedTimeRanges属性

    [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    

    在KVO的回调里:

    if ([keyPath isEqualToString:@"loadedTimeRanges"]){
        NSArray *array = _playerItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
        NSLog(@"当前缓冲时间:%f",totalBuffer);
    }
    

    4、playbackBufferEmpty

    监听playbackBufferEmpty我们可以获取当缓存不够,视频加载不出来的情况:

    [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil]; 
    

    在KVO回调里:

    if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
     
        //some code show loading   
    }
    

    5、playbackLikelyToKeepUp

    playbackLikelyToKeepUpplaybackBufferEmpty是一对,用于监听缓存足够播放的状态

    [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
    /* ... */
    if([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {    
        //由于 AVPlayer 缓存不足就会自动暂停,所以缓存充足了需要手动播放,才能继续播放  
        [_player play];      
    }
    

    AVURLAsset

    播放视频只需一个url就能进行这样太不安全了,别人可以轻易的抓包盗链,为此我们需要为视频链接做一个请求头的认证,这个功能可以借助AVURLAsset完成。

    AVPlayerItem除了可以用URL初始化,还可以用AVAsset初始化,而AVAsset不能直接使用,我们看下AVURLAsset的一个初始化方法:

    /*!
    @param      URL
                An instance of NSURL that references a media resource.
    @param      options
                An instance of NSDictionary that contains keys for specifying options for the initialization of the AVURLAsset. See AVURLAssetPreferPreciseDurationAndTimingKey and AVURLAssetReferenceRestrictionsKey above.
    */
    + (instancetype)URLAssetWithURL:(NSURL *)URL options:(nullable NSDictionary<NSString *, id> *)options;
    

    AVURLAssetPreferPreciseDurationAndTimingKey.这个key对应的value是一个布尔值, 用来表明资源是否需要为时长的精确展示,以及随机时间内容的读取进行提前准备。

    除了这个苹果官方介绍的功能外,他还可以设置请求头,这个算是隐藏功能了,因为苹果并没有明说这个功能,我是费了很大劲找到的。

    NSMutableDictionary * headers = [NSMutableDictionary dictionary];
    [headers setObject:@"yourHeader"forKey:@"User-Agent"];
    self.urlAsset = [AVURLAsset   URLAssetWithURL:self.videoURL options:@{@"AVURLAssetHTTPHeaderFieldsKey" : headers}];
    // 初始化playerItem
    self.playerItem = [AVPlayerItem playerItemWithAsset:self.urlAsset];
    

    补充:后来得知这个参数是非公开的API,但是经多人测试项目上线不受影响。

    播放相关通知

    1、声音类:

    //声音被打断的通知(电话打来)
    AVAudioSessionInterruptionNotification
    //耳机插入和拔出的通知
    AVAudioSessionRouteChangeNotification
    

    根据userInfo判断具体状态

    2、播放类

    //播放完成
    AVPlayerItemDidPlayToEndTimeNotification
    //播放失败
    AVPlayerItemFailedToPlayToEndTimeNotification
    //异常中断
    AVPlayerItemPlaybackStalledNotification
    

    对于播放完成的通知我们可以这么写:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerMovieFinish:) name:AVPlayerItemDidPlayToEndTimeNotification object:[self.player currentItem]];
    

    3、系统状态

    //进入后台
    UIApplicationWillResignActiveNotification
    //返回前台
    UIApplicationDidBecomeActiveNotification
    

    提示:所有通知和KVO的使用我们都要记得在不用时remove掉。

    小结

    视频播放相关的知识比较多,细节的方面需要一点一点去扣。暂且写这么多吧,以后有需要会及时补充。
    参考:
    ZFPlayer
    AVPlayer那些坑
    如果还有什么不理解的可以简书私信问我,或者查看我写的Demo,欢迎star- ( ゜- ゜)つロ乾杯~

    相关文章

      网友评论

      • 梁森的简书:如何播放本地视频呢?
      • survivorsfyh:楼主,有个疑问,我当前 VC 已经销毁,但是我的音频还在播放是因为什么情况导致的呢
      • 小小的路灯:有demo吗
      • Summer琼:您好 为什么 WMV 打不开
      • 神州第一坑:rmvb格式的我试了试 播放不出来啊
      • 不要重名就好:写的不错
      • zhaihongxia:你好,AVPlayer不支持流媒体吗?
        CC_iOS:兄弟不知道你有没有遇到过,加载网络视频的时候卡顿主线 导致返回按钮点不了怎么破啊?
        zhaihongxia:@勇闯天涯茉莉花茶 嗯嗯,谢谢回答和提供的参考文章
        zhangferry:@zhaihongxia 多谢提问,我刚又查了下,其实是支持的,苹果有自己的HLS协议,但是由于协议本身的问题,延迟较大,所以主流的流媒体还是基于RTMP协议。文中应该改成AVPlayer不适合流媒体。
        参考文章:http://www.jianshu.com/p/2ce402a485ca

      本文标题:AVPlayer详解系列(一)参数设置

      本文链接:https://www.haomeiwen.com/subject/otiudxtx.html