iOS使用AVPlayer封装自定义播放器

作者: 左叶右玉 | 来源:发表于2017-02-07 14:07 被阅读3749次

    一、 前言

    1. AVPlayer本身并不显示视频!需要一个AVPlayerLayer播放层来显示视频,然后添加到父视图的layer中。
    2. AVPlayer只负责视频管理和调控!而视频资源是由AVPlayerItem提供的,每个AVPlayerItem对应一个视频地址。
    3. 待补充...

    二、 重要属性和方法

    1. AVplayerItem:###

    + (instancetype)playerItemWithURL:(NSURL *)URL; 初始化视频资源方法
    duration视频总时长 (有光CMTime结构体可以自己查下)
    status 视频资源的状态 (需要监听的属性)
    loadedTimeRanges 在线视频的缓冲进度 (需要监听的属性)
    playbackBufferEmpty 进行跳转后没数据 (可选监听)
    playbackLikelyToKeepUp 进行跳转后有数据 (可选监听)

    2. AVplayer:###

    rate 播放速度 常用判断播放状态 =1播放 =0暂停
    - (void)play; - (void)pause; 播放暂停
    - (void)seekToTime:(CMTime)time;跳转到指定时间
    - (CMTime)currentTime;获取当前播放进度
    - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;监听当前播放进度
    - (void)removeTimeObserver:(id)observer;移除监听 (销毁播放器的时候调用)
    - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;切换视频资源

    3. AVPlayerLayer###

    videoGravity 视频的填充方式

    4. AVQueuePlayer:这个是用于处理播放列表的操作,待学习...###

    三、 剩下的都在代码里了

    LPAVPlayer.h

    typedef NS_ENUM(NSInteger, LPAVPlayerStatus) {
        LPAVPlayerStatusReadyToPlay = 0, // 准备好播放
        LPAVPlayerStatusLoadingVideo,    // 加载视频
        LPAVPlayerStatusPlayEnd,         // 播放结束
        LPAVPlayerStatusCacheData,       // 缓冲视频
        LPAVPlayerStatusCacheEnd,        // 缓冲结束
        LPAVPlayerStatusPlayStop,        // 播放中断 (多是没网)
        LPAVPlayerStatusItemFailed,      // 视频资源问题
        LPAVPlayerStatusEnterBack,       // 进入后台
        LPAVPlayerStatusBecomeActive,    // 从后台返回
    };
    
    @protocol LPAVPlayerDelegate <NSObject>
    
    @optional
    // 数据刷新
    - (void)refreshDataWith:(NSTimeInterval)totalTime Progress:(NSTimeInterval)currentTime LoadRange:(NSTimeInterval)loadTime;
    // 状态/错误 提示
    - (void)promptPlayerStatusOrErrorWith:(LPAVPlayerStatus)status;
    
    @end
    
    @interface LPAVPlayer : UIView
    
    @property (nonatomic, weak) id<LPAVPlayerDelegate>delegate;
    
    
    // 视频总长度
    @property (nonatomic, assign) NSTimeInterval totalTime;
    // 视频总长度
    //@property (nonatomic, assign) NSTimeInterval currentTime;
    // 缓存数据
    @property (nonatomic, assign) NSTimeInterval loadRange;
    
    
    /**
     准备播放器
     
     @param videoPath 视频地址
     */
    //- (void)setupPlayerWith:(NSString *)videoPath;
    - (void)setupPlayerWith:(NSURL *)videoURL;
    
    /** 播放 */
    - (void)play;
    
    /** 暂停 */
    - (void)pause;
    
    /** 播放|暂停 */
    - (void)playOrPause:(void (^)(BOOL isPlay))block;
    
    /** 拖动视频进度 */
    - (void)seekPlayerTimeTo:(NSTimeInterval)time;
    
    /** 跳动中不监听 */
    - (void)startToSeek;
    
    /**
     切换视频
     
     @param videoPath 视频地址
     */
    //- (void)replacePalyerItem:(NSString *)videoPath;
    - (void)replacePalyerItem:(NSURL *)videoURL;
    
    
    @end
    
    
    
    

    LPAVPlayer.m

    #import <AVFoundation/AVFoundation.h>
    
    @interface LPAVPlayer ()
    
    /** 播放器 */
    @property (nonatomic, strong) AVPlayer *player;
    /** 视频资源 */
    @property (nonatomic, strong) AVPlayerItem *currentItem;
    /** 播放器观察者 */
    @property (nonatomic ,strong)  id timeObser;
    // 拖动进度条的时候停止刷新数据
    @property (nonatomic ,assign) BOOL isSeeking;
    // 是否需要缓冲
    @property (nonatomic, assign) BOOL isCanPlay;
    // 是否需要缓冲
    @property (nonatomic, assign) BOOL needBuffer;
    
    @end
    
    @implementation LPAVPlayer
    
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self) {
            
            self.backgroundColor = [UIColor lightGrayColor];
            
            self.isCanPlay = NO;
            self.needBuffer = NO;
            self.isSeeking = NO;
            /**
             * 这里view用来做AVPlayer的容器
             * 完成对AVPlayer的二次封装
             * 要求 : 
             * 1. 暴露视频输出的API  视频时长 当前播放时间 进度
             * 2. 暴露出易于控制的data入口  播放 暂停 进度拖动 音量 亮度 清晰度调节
             */
            
            
        }
        return self;
    }
    
    #pragma mark - 属性和方法
    - (NSTimeInterval)totalTime
    {
        return CMTimeGetSeconds(self.player.currentItem.duration);
    }
    
    
    
    /** 
     准备播放器
     
     @param videoPath 视频地址
     */
    - (void)setupPlayerWith:(NSURL *)videoURL
    {
        [self creatPlayer:videoURL];
        
        [_player play];
        [self useDelegateWith:LPAVPlayerStatusLoadingVideo];
    }
    
    /**
     avplayer自身有一个rate属性
     rate ==1.0,表示正在播放;rate == 0.0,暂停;rate == -1.0,播放失败
     */
    
    /** 播放 */
    - (void)play
    {
        if (self.player.rate == 0) {
            [self.player play];
        }
    }
    
    /** 暂停 */
    - (void)pause
    {
        if (self.player.rate == 1.0) {
            [self.player pause];
        }
    }
    
    /** 播放|暂停 */
    - (void)playOrPause:(void (^)(BOOL isPlay))block;
    {
        if (self.player.rate == 0) {
            
            [self.player play];
            
            block(YES);
            
        }else if (self.player.rate == 1.0) {
            
            [self.player pause];
            
            block(NO);
            
        }else {
            NSLog(@"播放器出错");
        }
    }
    
    /** 拖动视频进度 */
    - (void)seekPlayerTimeTo:(NSTimeInterval)time
    {
        [self pause];
        [self startToSeek];
        __weak typeof(self)weakSelf = self;
        
        [self.player seekToTime:CMTimeMake(time, 1.0) completionHandler:^(BOOL finished) {
            [weakSelf endSeek];
            [weakSelf play];
        }];
        
    }
    
    /** 跳动中不监听 */
    - (void)startToSeek
    {
        self.isSeeking = YES;
    }
    - (void)endSeek
    {
        self.isSeeking = NO;
    }
    
    /**
     切换视频
     
     @param videoURL 视频地址
     */
    - (void)replacePalyerItem:(NSURL *)videoURL
    {
        self.isCanPlay = NO;
        
        [self pause];
        [self removeNotification];
        [self removeObserverWithPlayItem:self.currentItem];
        
        self.currentItem = [self getPlayerItem:videoURL];
        [self.player replaceCurrentItemWithPlayerItem:self.currentItem];
        [self addObserverWithPlayItem:self.currentItem];
        [self addNotificatonForPlayer];
        
        [self play];
        
    }
    
    
    /**
      播放状态代理调用
      
      @param status 播放状态
     */
    - (void)useDelegateWith:(LPAVPlayerStatus)status
    {
        if (self.isCanPlay == NO) {
            return;
        }
        
        if (self.delegate && [self.delegate respondsToSelector:@selector(promptPlayerStatusOrErrorWith:)]) {
            [self.delegate promptPlayerStatusOrErrorWith:status];
        }
    }
    
    
    #pragma mark - 创建播放器
    /**
     获取播放item
     
     @param videoURL 视频网址
     
     @return AVPlayerItem 
     */
    - (AVPlayerItem *)getPlayerItem:(NSURL *)videoURL
    {
        // 转utf8 防止中文报错
    //    videoPath = [videoPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    //    videoPath = [videoPath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
        
        // NSURL *url = [NSURL URLWithString:videoPath];
        // 如果播放本地视频要用 NSURL *url = [NSURL fileURLWithPath:videoURL];
        // 所以替换成URL
        
        AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
        
        return item;
    }
    
    
    /**
      创建播放器
     
     
     */
    - (void)creatPlayer:(NSURL *)videoURL
    {
        if (!_player) {
            
            self.currentItem = [self getPlayerItem:videoURL];
            
            _player = [AVPlayer playerWithPlayerItem:self.currentItem];
            
            [self creatPlayerLayer];
            
            [self addPlayerObserver];
            
            [self addObserverWithPlayItem:self.currentItem];
            
            [self addNotificatonForPlayer];
        }
    }
    
    /**
     创建播放器 layer 层
     AVPlayerLayer的videoGravity属性设置
     AVLayerVideoGravityResize,       // 非均匀模式。两个维度完全填充至整个视图区域
     AVLayerVideoGravityResizeAspect,  // 等比例填充,直到一个维度到达区域边界
     AVLayerVideoGravityResizeAspectFill, // 等比例填充,直到填充满整个视图区域,其中一个维度的部分区域会被裁剪
     */
    - (void)creatPlayerLayer
    {
        AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
        
        layer.frame = self.bounds;
        
        layer.videoGravity = AVLayerVideoGravityResizeAspect;
        
        [self.layer addSublayer:layer];
    }
    
    #pragma mark - 添加 监控
    /** 给player 添加 time observer */
    - (void)addPlayerObserver
    {
        __weak typeof(self)weakSelf = self;
        _timeObser = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            AVPlayerItem *playerItem = weakSelf.player.currentItem;
            
            float current = CMTimeGetSeconds(time);
            
            float total = CMTimeGetSeconds([playerItem duration]);
            
            if (weakSelf.isSeeking) {
                return;
            }
            
            if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(refreshDataWith:Progress:LoadRange:)]) {
                [weakSelf.delegate refreshDataWith:total Progress:current LoadRange:weakSelf.loadRange];
            }
    //        NSLog(@"当前播放进度 %.2f/%.2f.",current,total);
            
        }];
    }
    /** 移除 time observer */
    - (void)removePlayerObserver
    {
        [_player removeTimeObserver:_timeObser];
    }
    
    /** 给当前播放的item 添加观察者 
     
     需要监听的字段和状态
     status :  AVPlayerItemStatusUnknown,AVPlayerItemStatusReadyToPlay,AVPlayerItemStatusFailed
     loadedTimeRanges  :  缓冲进度
     playbackBufferEmpty : seekToTime后,缓冲数据为空,而且有效时间内数据无法补充,播放失败
     playbackLikelyToKeepUp : seekToTime后,可以正常播放,相当于readyToPlay,一般拖动滑竿菊花转,到了这个这个状态菊花隐藏
     
     */
    - (void)addObserverWithPlayItem:(AVPlayerItem *)item
    {
        [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
        [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
        [item addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
        [item addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
    }
    /** 移除 item 的 observer */
    - (void)removeObserverWithPlayItem:(AVPlayerItem *)item
    {
        [item removeObserver:self forKeyPath:@"status"];
        [item removeObserver:self forKeyPath:@"loadedTimeRanges"];
        [item removeObserver:self forKeyPath:@"playbackBufferEmpty"];
        [item removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
    }
    /** 数据处理 获取到观察到的数据 并进行处理 */
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        AVPlayerItem *item = object;
        if ([keyPath isEqualToString:@"status"]) {// 播放状态
            
            [self handleStatusWithPlayerItem:item];
            
        } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {// 缓冲进度
            
            [self handleLoadedTimeRangesWithPlayerItem:item];
            
        } else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {// 跳转后没数据
            
            // 转菊花
            if (self.isCanPlay) {
                NSLog(@"跳转后没数据");
                self.needBuffer = YES;
                [self useDelegateWith:LPAVPlayerStatusCacheData];
            }
            
            
            
        } else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {// 跳转后有数据
            
            
            
            // 隐藏菊花
            if (self.isCanPlay && self.needBuffer) {
                
                NSLog(@"跳转后有数据");
                
                self.needBuffer = NO;
                
                [self useDelegateWith:LPAVPlayerStatusCacheEnd];
            }
            
        }
    }
    /**
      处理 AVPlayerItem 播放状态
     AVPlayerItemStatusUnknown           状态未知
     AVPlayerItemStatusReadyToPlay       准备好播放
     AVPlayerItemStatusFailed            播放出错
     */
    - (void)handleStatusWithPlayerItem:(AVPlayerItem *)item
    {
        AVPlayerItemStatus status = item.status;
        switch (status) {
            case AVPlayerItemStatusReadyToPlay:   // 准备好播放
                
                NSLog(@"AVPlayerItemStatusReadyToPlay");
                self.isCanPlay = YES;
                [self useDelegateWith:LPAVPlayerStatusReadyToPlay];
                
                break;
            case AVPlayerItemStatusFailed:        // 播放出错
                
                NSLog(@"AVPlayerItemStatusFailed");
                [self useDelegateWith:LPAVPlayerStatusItemFailed];
                
                break;
            case AVPlayerItemStatusUnknown:       // 状态未知
                
                NSLog(@"AVPlayerItemStatusUnknown");
                
                break;
                
            default:
                break;
        }
        
    }
    /** 处理缓冲进度 */
    - (void)handleLoadedTimeRangesWithPlayerItem:(AVPlayerItem *)item
    {
        NSArray *loadArray = item.loadedTimeRanges;
        
        CMTimeRange range = [[loadArray firstObject] CMTimeRangeValue];
        
        float start = CMTimeGetSeconds(range.start);
        
        float duration = CMTimeGetSeconds(range.duration);
        
        NSTimeInterval totalTime = start + duration;// 缓存总长度
        
        _loadRange = totalTime;
    //    NSLog(@"缓冲进度 -- %.2f",totalTime);
        
    }
    
    
    /**
     添加关键通知
     
     AVPlayerItemDidPlayToEndTimeNotification     视频播放结束通知
     AVPlayerItemTimeJumpedNotification           视频进行跳转通知
     AVPlayerItemPlaybackStalledNotification      视频异常中断通知
     UIApplicationDidEnterBackgroundNotification  进入后台
     UIApplicationDidBecomeActiveNotification     返回前台
     
     */
    - (void)addNotificatonForPlayer
    {
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        
        [center addObserver:self selector:@selector(videoPlayEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    //    [center addObserver:self selector:@selector(videoPlayToJump:) name:AVPlayerItemTimeJumpedNotification object:nil];//没意义
        [center addObserver:self selector:@selector(videoPlayError:) name:AVPlayerItemPlaybackStalledNotification object:nil];
        [center addObserver:self selector:@selector(videoPlayEnterBack:) name:UIApplicationDidEnterBackgroundNotification object:nil];
        [center addObserver:self selector:@selector(videoPlayBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
    }
    /** 移除 通知 */
    - (void)removeNotification
    {
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        
        [center removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    //    [center removeObserver:self name:AVPlayerItemTimeJumpedNotification object:nil];
        [center removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil];
        [center removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
        [center removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
        [center removeObserver:self];
    }
    
    /** 视频播放结束 */
    - (void)videoPlayEnd:(NSNotification *)notic
    {
        NSLog(@"视频播放结束");
        [self useDelegateWith:LPAVPlayerStatusPlayEnd];
        [self.player seekToTime:kCMTimeZero];
    }
    ///** 视频进行跳转 */ 没有意义的方法 会被莫名的多次调动,不清楚机制
    //- (void)videoPlayToJump:(NSNotification *)notic
    //{
    //    NSLog(@"视频进行跳转");
    //}
    /** 视频异常中断 */
    - (void)videoPlayError:(NSNotification *)notic
    {
        NSLog(@"视频异常中断");
        [self useDelegateWith:LPAVPlayerStatusPlayStop];
    }
    /** 进入后台 */
    - (void)videoPlayEnterBack:(NSNotification *)notic
    {
        NSLog(@"进入后台");
        [self useDelegateWith:LPAVPlayerStatusEnterBack];
    }
    /** 返回前台 */
    - (void)videoPlayBecomeActive:(NSNotification *)notic
    {
        NSLog(@"返回前台");
        [self useDelegateWith:LPAVPlayerStatusBecomeActive];
    }
    
    #pragma mark - 销毁 release
    - (void)dealloc
    {
        NSLog(@"--- %@ --- 销毁了",[self class]);
        
        [self removeNotification];
        [self removePlayerObserver];
        [self removeObserverWithPlayItem:self.player.currentItem];
        
    }
    
    @end
    
    

    四、 在控制器中的调用

    因为控制器还有一些其他的代码,就不整个贴出来了,截取一部分重要的贴出来。

    1. 创建播放器View

    /**
     创建播放器视图
    */
    - (void)buildPlayerView
    {
       CGRect rect = CGRectMake(0, 64, self.view.bounds.size.width, 250);
       
       _playView = [[LPAVPlayer alloc] initWithFrame:rect];
       
       _playView.delegate = self;
       
       [self.view addSubview:_playView];
       
       [_playView setupPlayerWith:url];
       
       
    }
    
    

    2. 代理 -- 状态提示

    // LPAVPlayer delegate  ----- 状态提示
    - (void)promptPlayerStatusOrErrorWith:(LPAVPlayerStatus)status
    {
      switch (status) {
          case LPAVPlayerStatusLoadingVideo:// 开始准备
              
              [self.activity startAnimating];
              
              break;
              
          case LPAVPlayerStatusReadyToPlay:// 准备完成
              
              [self.activity stopAnimating];
              [self.playOrPause setTitle:@"暂停" forState:UIControlStateNormal];
              self.slider.maximumValue = _playView.totalTime;
              self.slider.value = 0;
              self.loadProgress.progress = 0;
              
              break;
              
              
              
          default:
              break;
      }
    }
    
    

    3. 代理 -- 刷新数据

    // LPAVPlayer delegate  ----- 刷新数据
    - (void)refreshDataWith:(NSTimeInterval)totalTime Progress:(NSTimeInterval)currentTime LoadRange:(NSTimeInterval)loadTime
    {
      
      [self.loadProgress setProgress:(loadTime/totalTime) animated:YES];
      
      [self.slider setValue:currentTime animated:YES];
      
      self.timeLabel.text = [NSString stringWithFormat:@"%@/%@",[self otherConvertTimeFormat:currentTime],[self otherConvertTimeFormat:totalTime]];
    }
    
    - (NSString *)otherConvertTimeFormat:(NSInteger)time
    {
      NSString *needStr = @"";
      
      if (time < 3600) {
          
          needStr =  [NSString stringWithFormat:@"%02li:%02li",lround(floor(time/60.f)),lround(floor(time/1.f))%60];
          
      } else {
          
          needStr =  [NSString stringWithFormat:@"%02li:%02li:%02li",lround(floor(time/3600.f)),lround(floor(time%3600)/60.f),lround(floor(time/1.f))%60];
          
      }
      return needStr;
    }
    
    

    五、 本文参考的文章

    作者:夜千寻墨 - AVPlayer
    作者:SomeBoy - iOS开发AVPlayer深入浅出
    作者:KenshinCui iOS开发系列--音频播放、录音、视频播放、拍照、视频录制

    相关文章

      网友评论

      • MoussyL:作者好,AVPlayerItem 的 preferredForwardBufferDuration 这个属性,有用过么 ?
        MoussyL:@左叶右玉 谢谢作者的回复,解释的很详细。:pray:
        左叶右玉:没用过,不过可以看下官方的解释https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration 。大致意思应该是当前播放时间往后多缓存多少秒的视频数据。设置为0的话是根据系统和网络环境自动选择;另外设置数值越小缓冲的视频时间越少,设置的时间越大缓冲的视频数据越大。一般都不处理吧。你设置的数值小的话,如果网络差视频会频繁的缓冲。如果数值设置的大的话,就消耗更多系统资源。大概是这个意思。
      • YimG:666666666666666666666666666666666666
      • CocoaYog:写的很清晰!不错的文章
        左叶右玉:谢谢!
      • 7a03a6b61c9f:博主, 有没有Demo
        左叶右玉:这就是demo啊,所有代码都在这呢。

      本文标题:iOS使用AVPlayer封装自定义播放器

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