iOS開發-封裝AVPlayer播放網絡、本地視頻

AVPlayer.png

前言:說到視頻播放器,相信大家基本都能想到AVPlayer,使用AVPlayer簡單的幾行代碼就可以實現本地和網絡視頻的播放。如果要實現稍微復雜點的功能,比如說增加進度條,全屏按鈕等,如果把這些都寫在ViewController里邊的話會使ViewController顯得代碼比較冗雜。基于此,小編在使用AVPlayer時進行了封裝,實現了播放進度時間展示、續播、緩沖進度條、進度條拖拽快進快退、多個視頻順序播放、全屏播放的功能。

原理:對AVPlayerItemloadedTimeRangesstatus兩個屬性的監聽實現緩沖進度和播放狀態的獲取;創建model保存要播放的視頻的信息并存儲在數組中來實現順序播放;對播放器的標題和工具欄進行封裝來降低定制view中的代碼量,并使用代理傳值進行回調。

先來看一下效果圖

AVPlayer.gif

下面我們來正式開始進行封裝:
首先,創建存儲視頻信息的model(大家可以根據自己需求進行修改)如下:

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, RHVideoPlayStyle) {
    
    RHVideoPlayStyleLocal = 0,       //播放本地視頻
    RHVideoPlayStyleNetwork,         //播放網絡視頻
    RHVideoPlayStyleNetworkSD,       //播放網絡標清視頻
    RHVideoPlayStyleNetworkHD,       //播放網絡高清視頻
};
@interface RHVideoModel : NSObject

@property (nonatomic, copy, readonly) NSString * videoId;
@property (nonatomic, copy, readonly) NSString * title;
@property (nonatomic, strong, readonly) NSURL * url;
@property (nonatomic, assign) RHVideoPlayStyle style;
@property (nonatomic, assign) NSTimeInterval currentTime;

/**
 創建本地視頻模型

 @param videoId     視頻ID
 @param title       標題
 @param videoPath   播放文件路徑
 @param currentTime 當前播放時間
 @return            本地視頻模型
 */
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title videoPath:(NSString *)videoPath currentTime:(NSTimeInterval)currentTime;

/**
 創建網絡視頻模型
 
 @param videoId     視頻ID
 @param title       標題
 @param url         視頻地址
 @param currentTime 當前播放時間
 @return            網絡視頻模型
 */
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title url:(NSString *)url currentTime:(NSTimeInterval)currentTime;

/**
 創建網絡視頻模型
 
 @param videoId     視頻ID
 @param title       標題
 @param sdUrl       標清地址
 @param hdUrl       高清地址
 @param currentTime 當前播放時間
 @return            網絡視頻模型
 */
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title sdUrl:(NSString *)sdUrl hdUrl:(NSString *)hdUrl currentTime:(NSTimeInterval)currentTime;
@end
#import "RHVideoModel.h"

@interface RHVideoModel ()

@property (nonatomic, copy) NSString * sdUrl;
@property (nonatomic, copy) NSString * hdUrl;
@end
@implementation RHVideoModel

- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title videoPath:(NSString *)videoPath currentTime:(NSTimeInterval)currentTime {
    
    self = [super init];
    
    if (self) {
        
        _videoId = [videoId copy];
        _title = [title copy];
        _currentTime = currentTime;
        _url = [[NSURL fileURLWithPath:videoPath] copy];
        _style = RHVideoPlayStyleLocal;
    }
    return self;
}

- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title url:(NSString *)url currentTime:(NSTimeInterval)currentTime {
    
    self = [super init];
    
    if (self) {
        
        _videoId = [videoId copy];
        _title = [title copy];
        _currentTime = currentTime;
        _url = [[NSURL URLWithString:url] copy];
        _style = RHVideoPlayStyleNetwork;
    }
    return self;
}

- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title sdUrl:(NSString *)sdUrl hdUrl:(NSString *)hdUrl currentTime:(NSTimeInterval)currentTime {
    
    self = [super init];
    
    if (self) {
        
        _videoId = [videoId copy];
        _title = [title copy];
        _currentTime = currentTime;
        _sdUrl = [sdUrl copy];
        _hdUrl = [hdUrl copy];
        self.style = RHVideoPlayStyleNetworkHD;
    }
    return self;
}

- (void)setStyle:(RHVideoPlayStyle)style {
    
    _style = style;
    
    if (_style == RHVideoPlayStyleNetworkSD) {
        
        _url = [[NSURL URLWithString:_sdUrl] copy];
        NSLog(@"%@", _sdUrl);
    } else if (_style == RHVideoPlayStyleNetworkHD) {
        
        _url = [[NSURL URLWithString:_hdUrl] copy];
        NSLog(@"%@", _hdUrl);
    }
}
@end

對此model的所有方法都已經注釋,在此不再做過多詳解。

接下來給大家說一下全屏的思想,我是在點擊全屏的時候,從當前的ViewController彈出一個新的ViewController并且將播放的view從之前的ViewController移除并添加到新的ViewController上邊,同時改變viewframe,新的ViewController為橫屏狀態即可實現全屏效果。先來看一下全屏的ViewController的實現只需要創建一個繼承于UIViewController的類,在.m中重寫兩個方法如下:

#import "RHFullViewController.h"

@interface RHFullViewController ()

@end

@implementation RHFullViewController

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    
    return UIInterfaceOrientationMaskLandscape;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
    
    return YES;
}
@end

由于AVPlayer的播放顯示效果是在AVPlayerLayer上邊,所以小編寫了一個RHPlayerLayerView來添加AVPlayerLayer并讓AVPlayerLayerframe跟隨RHPlayerLayerViewframe的改變來改變,這樣只需要對該RHPlayerLayerViewframe來進行修改即可。如下:

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface RHPlayerLayerView : UIView

- (void)addPlayerLayer:(AVPlayerLayer *)playerLayer;
@end
#import "RHPlayerLayerView.h"

@interface RHPlayerLayerView ()

@property (nonatomic, strong) AVPlayerLayer * playerLayer;
@end
@implementation RHPlayerLayerView

- (void)addPlayerLayer:(AVPlayerLayer *)playerLayer {
    
    _playerLayer = playerLayer;
    playerLayer.backgroundColor = [UIColor blackColor].CGColor;
    _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    _playerLayer.contentsScale = [UIScreen mainScreen].scale;
    [self.layer addSublayer:_playerLayer];
}

- (void)layoutSublayersOfLayer:(CALayer *)layer {
    
    [super layoutSublayersOfLayer:layer];
    
    _playerLayer.frame = self.bounds;
}
@end

對于播放器上邊的標題和控制欄以及播放失敗顯示頁面的封裝在此就不多說了,主要使用的是代理回調來傳值控制播放器的。

接下來,我們重點來說對于AVPlayer的封裝:
首先創建RHPlayerView繼承于UIView,在RHPlayerView.h中定義方法如下:

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "RHVideoModel.h"

@protocol RHPlayerViewDelegate;
@interface RHPlayerView : UIView

@property (nonatomic, weak) id<RHPlayerViewDelegate> delegate;

/**
 對象方法創建對象

 @param frame      約束
 @param controller 所在的控制器
 @return           對象
 */
- (instancetype)initWithFrame:(CGRect)frame currentVC:(UIViewController *)controller;

/**
 設置要播放的視頻列表和要播放的視頻

 @param videoModels 存儲視頻model的數組
 @param videoId     當前要播放的視頻id
 */
- (void)setVideoModels:(NSArray<RHVideoModel *> *)videoModels playVideoId:(NSString *)videoId;

/**
 設置覆蓋的圖片

 @param imageUrl 覆蓋的圖片url
 */
- (void)setCoverImage:(NSString *)imageUrl;

/**
 點擊目錄要播放的視頻id

 @param videoId 要不放的視頻id
 */
- (void)playVideoWithVideoId:(NSString *)videoId;

/**
 暫停
 */
- (void)pause;

/**
 停止
 */
- (void)stop;

@end
@protocol RHPlayerViewDelegate <NSObject>

// 是否可以播放
- (BOOL)playerViewShouldPlay;

@optional
// 播放結束
- (void)playerView:(RHPlayerView *)playView didPlayEndVideo:(RHVideoModel *)videoModel index:(NSInteger)index;
// 開始播放
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel index:(NSInteger)index;
// 播放中
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel playTime:(NSTimeInterval)playTime;
@end

所有的方法都添加了注釋,相信大家都能一目了然,在此小編給該view添加了代理,這樣可以在ViewController中控制播放器的播放并實時獲取播放進度及播放的視頻信息。

接下來我們來看一下在RHPlayerView.m中的實現,由于添加的功能比較多,所以這里的代碼比較多一些,希望大家能夠耐心一些,其中的titleViewtoolViewfailedView分別是定制的播放器上方的標題欄、下方的控制欄和播放失敗顯示的視圖,大家在此可以暫時忽略這些,具體代碼如下:

#import "RHPlayerView.h"
#import "RHFullViewController.h"
#import "RHPlayerTitleView.h"
#import "RHPlayerToolView.h"
#import "RHPlayerFailedView.h"
#import "RHPlayerLayerView.h"

@interface RHPlayerView () <RHPlayerToolViewDelegate, RHPlayerTitleViewDelegate, RHPlayerFailedViewDelegate>

@property (nonatomic, strong) AVPlayer * player;
@property (nonatomic, strong) AVPlayerItem * playerItem;
@property (nonatomic, strong) AVPlayerLayer * playerLayer;

@property (nonatomic, strong) RHFullViewController * fullVC;
@property (nonatomic, weak) UIViewController * currentVC;

@property (nonatomic, strong) RHPlayerTitleView * titleView;
@property (nonatomic, strong) RHPlayerToolView * toolView;
@property (nonatomic, strong) RHPlayerFailedView * failedView;
@property (nonatomic, strong) RHPlayerLayerView * layerView;
@property (nonatomic, strong) UIActivityIndicatorView * activity;
@property (nonatomic, strong) UIImageView * coverImageView;

@property (nonatomic, strong) CADisplayLink * link;
@property (nonatomic, assign) NSTimeInterval lastTime;

@property (nonatomic, strong) NSTimer * toolViewShowTimer;
@property (nonatomic, assign) NSTimeInterval toolViewShowTime;

// 當前是否顯示控制條
@property (nonatomic, assign) BOOL isShowToolView;
// 是否第一次播放
@property (nonatomic, assign) BOOL isFirstPlay;
// 是否重播
@property (nonatomic, assign) BOOL isReplay;

@property (nonatomic, strong) NSArray * videoArr;
@property (nonatomic, strong) RHVideoModel * videoModel;

@property (nonatomic) CGRect playerFrame;
@end
@implementation RHPlayerView

#pragma mark - public

// 初始化方法
- (instancetype)initWithFrame:(CGRect)frame currentVC:(UIViewController *)controller {
    
    self = [super initWithFrame:frame];
    
    if (self) {
        
        self.clipsToBounds = YES;
        self.backgroundColor = [UIColor blackColor];
        self.currentVC = controller;
        _isShowToolView = YES;
        _isFirstPlay = YES;
        _isReplay = NO;
        _playerFrame = frame;
        [self addSubviews];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayEnd) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    }
    return self;
}
// 設置覆蓋的圖片
- (void)setCoverImage:(NSString *)imageUrl {
    
    _coverImageView.hidden = NO;
    [_coverImageView sd_setImageWithURL:[NSURL URLWithString:imageUrl] placeholderImage:[UIImage imageNamed:@""]];
}

// 設置要播放的視頻列表和要播放的視頻
- (void)setVideoModels:(NSArray<RHVideoModel *> *)videoModels playVideoId:(NSString *)videoId {
    
    self.videoArr = [NSArray arrayWithArray:videoModels];
    
    if (videoId.length > 0) {
        
        for (RHVideoModel * model in self.videoArr) {
            
            if ([model.videoId isEqualToString:videoId]) {
                
                NSInteger index = [self.videoArr indexOfObject:model];
                self.videoModel = self.videoArr[index];
                break;
            }
        }
    } else {
        
        self.videoModel = self.videoArr.firstObject;
    }
    _titleView.title = self.videoModel.title;
    _isFirstPlay = YES;
}
// 點擊目錄要播放的視頻id
- (void)playVideoWithVideoId:(NSString *)videoId {
    
    if (![self.delegate respondsToSelector:@selector(playerViewShouldPlay)]) {
        
        return;
    }
    [self.delegate playerViewShouldPlay];
    
    for (RHVideoModel * model in self.videoArr) {
        
        if ([model.videoId isEqualToString:videoId]) {
            
            NSInteger index = [self.videoArr indexOfObject:model];
            self.videoModel = self.videoArr[index];
            break;
        }
    }
    _titleView.title = self.videoModel.title;

    if (_isFirstPlay) {
        
        _coverImageView.hidden = YES;
        [self setPlayer];
        [self addToolViewTimer];
        
        _isFirstPlay = NO;
    } else {
        
        [self.player pause];
        [self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
        [self addToolViewTimer];
    }
}
// 暫停
- (void)pause {
    
    [self.player pause];
    self.link.paused = YES;
    _toolView.playSwitch.selected = NO;
    [self removeToolViewTimer];
}
// 停止
- (void)stop {
    
    [self.player pause];
    [self.link invalidate];
    _toolView.playSwitch.selected = NO;
    [self removeToolViewTimer];
}

#pragma mark - add subviews and make constraints

- (void)addSubviews {
    
    // 播放的layerView
    [self addSubview:self.layerView];
    // 菊花
    [self addSubview:self.activity];
    // 加載失敗
    [self addSubview:self.failedView];
    // 覆蓋的圖片
    [self addSubview:self.coverImageView];
    // 下部工具欄
    [self addSubview:self.toolView];
    // 上部標題欄
    [self addSubview:self.titleView];
    // 添加約束
    [self makeConstraintsForUI];
}

- (void)makeConstraintsForUI {
    
    [_layerView mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.top.mas_equalTo(@0);
        make.left.mas_equalTo(@0);
        make.right.mas_equalTo(@0);
        make.bottom.mas_equalTo(@0);
    }];
    
    [_toolView mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.bottom.mas_equalTo(@0);
        make.left.mas_equalTo(@0);
        make.right.mas_equalTo(@0);
        make.height.mas_equalTo(@44);
    }];
    
    [_titleView mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.top.mas_equalTo(@0);
        make.left.mas_equalTo(@0);
        make.right.mas_equalTo(@0);
        make.height.mas_equalTo(@44);
    }];
    
    [_activity mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.size.mas_equalTo(CGSizeMake(30, 30));
        make.centerX.mas_equalTo(self.mas_centerX);
        make.centerY.mas_equalTo(self.mas_centerY);
    }];
    
    [_failedView mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.top.mas_equalTo(@0);
        make.left.mas_equalTo(@0);
        make.right.mas_equalTo(@0);
        make.bottom.mas_equalTo(@0);
    }];
    
    [_coverImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.top.mas_equalTo(@0);
        make.left.mas_equalTo(@0);
        make.right.mas_equalTo(@0);
        make.bottom.mas_equalTo(@0);
    }];
}

- (void)layoutSubviews {
    
    [self.superview bringSubviewToFront:self];
}

#pragma mark - notification

// 視頻播放完成通知
- (void)videoPlayEnd {
    
    NSLog(@"播放完成");
    
    _toolView.playSwitch.selected = NO;
    
    [UIView animateWithDuration:0.25 animations:^{
        
        [_toolView mas_updateConstraints:^(MASConstraintMaker *make) {
            
            make.bottom.mas_equalTo(@0);
        }];
        [_titleView mas_updateConstraints:^(MASConstraintMaker *make) {
           
            make.top.mas_equalTo(@0);
        }];
        [self layoutIfNeeded];
    } completion:^(BOOL finished) {
        
        _isShowToolView = YES;
    }];
    
    self.videoModel.currentTime = 0;
    NSInteger index = [self.videoArr indexOfObject:self.videoModel];
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayEndVideo:index:)]) {
        
        [self.delegate playerView:self didPlayEndVideo:self.videoModel index:index];
    }
    
    if (index != self.videoArr.count - 1) {
        
        [self.player pause];
        self.videoModel = self.videoArr[index + 1];
        _titleView.title = self.videoModel.title;
        [self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
        [self addToolViewTimer];
    } else {
        
        _isReplay = YES;
        [self.player pause];
        self.link.paused = YES;
        [self removeToolViewTimer];
        _coverImageView.hidden = NO;
        _toolView.slider.sliderPercent = 0;
        _toolView.slider.enabled = NO;
        [_activity stopAnimating];
    }
}

#pragma mark - 監聽視頻緩沖和加載狀態
//注冊觀察者監聽狀態和緩沖
- (void)addObserverWithPlayerItem:(AVPlayerItem *)playerItem {
    
    if (playerItem) {
        
        [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
        [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    }
}

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

// 監聽變化方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    AVPlayerItem * playerItem = (AVPlayerItem *)object;
    
    if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        
        NSTimeInterval loadedTime = [self availableDurationWithplayerItem:playerItem];
        NSTimeInterval totalTime = CMTimeGetSeconds(playerItem.duration);
        
        if (!_toolView.slider.isSliding) {
            
            _toolView.slider.progressPercent = loadedTime/totalTime;
        }
        
    } else if ([keyPath isEqualToString:@"status"]) {
        
        if (playerItem.status == AVPlayerItemStatusReadyToPlay) {
            
            NSLog(@"playerItem is ready");
            
            [self.player play];
            self.link.paused = NO;
            CMTime seekTime = CMTimeMake(self.videoModel.currentTime, 1);
            [self.player seekToTime:seekTime completionHandler:^(BOOL finished) {
                
                if (finished) {
                    
                    NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
                    _toolView.currentTimeLabel.text = [self convertTimeToString:current];
                }
            }];
            _toolView.slider.enabled = YES;
            _toolView.playSwitch.enabled = YES;
            _toolView.playSwitch.selected = YES;
        } else{
            
            NSLog(@"load break");
            self.failedView.hidden = NO;
        }
    }
}

#pragma mark - private

// 設置播放器
- (void)setPlayer {
    
    if (self.videoModel) {
        
        if (self.videoModel.url) {
            
            if (![self checkNetwork]) {
                
                return;
            }
            AVPlayerItem * item = [AVPlayerItem playerItemWithURL:self.videoModel.url];
            self.playerItem = item;
            [self addObserverWithPlayerItem:self.playerItem];
            
            if (self.player) {
                
                [self.player replaceCurrentItemWithPlayerItem:self.playerItem];
            } else {
                
                self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
            }
            self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
            [_layerView addPlayerLayer:self.playerLayer];
            
            NSInteger index = [self.videoArr indexOfObject:self.videoModel];
            if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayVideo:index:)]) {
                
                [self.delegate playerView:self didPlayVideo:self.videoModel index:index];
            }
            self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateSlider)];
            [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        } else {
            
            _failedView.hidden = NO;
        }
        
    } else {
        
        _failedView.hidden = NO;
    }
}

//切換當前播放的內容
- (void)replaceCurrentPlayerItemWithVideoModel:(RHVideoModel *)model {
    
    if (self.player) {
        
        if (model) {
            
            if (![self checkNetwork]) {
                
                return;
            }
            //由暫停狀態切換時候 開啟定時器,將暫停按鈕狀態設置為播放狀態
            self.link.paused = NO;
            _toolView.playSwitch.selected = YES;
            
            //移除當前AVPlayerItem對"loadedTimeRanges"和"status"的監聽
            [self removeObserverWithPlayerItem:self.playerItem];
            
            if (model.url) {
                
                AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:model.url];
                self.playerItem = playerItem;
                [self addObserverWithPlayerItem:self.playerItem];
                //更換播放的AVPlayerItem
                [self.player replaceCurrentItemWithPlayerItem:self.playerItem];
                NSInteger index = [self.videoArr indexOfObject:self.videoModel];
                if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayVideo:index:)]) {
                    
                    [self.delegate playerView:self didPlayVideo:self.videoModel index:index];
                }
                _toolView.playSwitch.enabled = NO;
                _toolView.slider.enabled = NO;
            } else {
                
                _toolView.playSwitch.selected = NO;
                _failedView.hidden = NO;
            }
            
        } else {
            
            _toolView.playSwitch.selected = NO;
            _failedView.hidden = NO;
        }
    } else {
        
        _toolView.playSwitch.selected = NO;
        _failedView.hidden = NO;
    }
}

//轉換時間成字符串
- (NSString *)convertTimeToString:(NSTimeInterval)time {
    
    if (time <= 0) {
        
        return @"00:00";
    }
    int minute = time / 60;
    int second = (int)time % 60;
    NSString * timeStr;
    
    if (minute >= 100) {
        
        timeStr = [NSString stringWithFormat:@"%d:%02d", minute, second];
    }else {
        
        timeStr = [NSString stringWithFormat:@"%02d:%02d", minute, second];
    }
    return timeStr;
}

// 獲取緩沖進度
- (NSTimeInterval)availableDurationWithplayerItem:(AVPlayerItem *)playerItem {
    
    NSArray * loadedTimeRanges = [playerItem loadedTimeRanges];
    // 獲取緩沖區域
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];
    NSTimeInterval startSeconds = CMTimeGetSeconds(timeRange.start);
    NSTimeInterval durationSeconds = CMTimeGetSeconds(timeRange.duration);
    // 計算緩沖總進度
    NSTimeInterval result = startSeconds + durationSeconds;
    return result;
}

- (void)addToolViewTimer {

    [self removeToolViewTimer];
    _toolViewShowTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(updateToolViewShowTime) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:_toolViewShowTimer forMode:NSRunLoopCommonModes];
}

- (void)removeToolViewTimer {

    [_toolViewShowTimer invalidate];
    _toolViewShowTimer = nil;
    _toolViewShowTime = 0;
}

- (BOOL)checkNetwork {
    
    // 這里做網絡監測
    return YES;
}

#pragma mark - slider event

- (void)progressValueChange:(RHProgressSlider *)slider {
    
    [self addToolViewTimer];
    if (self.player.status == AVPlayerStatusReadyToPlay) {
        
        NSTimeInterval duration = slider.sliderPercent * CMTimeGetSeconds(self.player.currentItem.duration);
        CMTime seekTime = CMTimeMake(duration, 1);
        
        [self.player seekToTime:seekTime completionHandler:^(BOOL finished) {
            
            if (finished) {
                
                NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
                 _toolView.currentTimeLabel.text = [self convertTimeToString:current];
            }
        }];
    }
}

#pragma mark - timer event
// 更新進度條
- (void)updateSlider {
    
    NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
    NSTimeInterval total = CMTimeGetSeconds(self.player.currentItem.duration);
    //如果用戶在手動滑動滑塊,則不對滑塊的進度進行設置重繪
    if (!_toolView.slider.isSliding) {
        
        _toolView.slider.sliderPercent = current/total;
    }
    
    if (current != self.lastTime) {
        
        [_activity stopAnimating];
        _toolView.currentTimeLabel.text = [self convertTimeToString:current];
        _toolView.totleTimeLabel.text = isnan(total) ? @"00:00" : [self convertTimeToString:total];
        
        if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayVideo:playTime:)]) {
            
            [self.delegate playerView:self didPlayVideo:self.videoModel playTime:current];
        }
    }else{
        
        [_activity startAnimating];
    }
    // 記錄當前播放時間 用于區分是否卡頓顯示緩沖動畫
    self.lastTime = current;
}

- (void)updateToolViewShowTime {
    
    _toolViewShowTime++;

    if (_toolViewShowTime == 5) {

        [self removeToolViewTimer];
        _toolViewShowTime = 0;
        [self showOrHideBar];
    }
}

#pragma mark - failedView delegate
// 重新播放
- (void)failedViewDidReplay:(RHPlayerFailedView *)failedView {
    
    _failedView.hidden = YES;
    [self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
}

#pragma mark - titleView delegate

- (void)titleViewDidExitFullScreen:(RHPlayerTitleView *)titleView {
    
    [_toolView exitFullScreen];
}

#pragma mark - toolView delegate

- (void)toolView:(RHPlayerToolView *)toolView playSwitch:(BOOL)isPlay {
    
    if (_isFirstPlay) {
        
        if (![self.delegate playerViewShouldPlay]) {
            
            _toolView.playSwitch.selected = !_toolView.playSwitch.selected;
            return;
        }
        
        _coverImageView.hidden = YES;
        if (!self.videoModel.videoId) {
            
            _coverImageView.hidden = NO;
            _toolView.playSwitch.selected = !_toolView.playSwitch.selected;
            return;
        }
        [self setPlayer];
        [self addToolViewTimer];
        
        _isFirstPlay = NO;
    } else if (_isReplay) {
        
        _coverImageView.hidden = YES;
        self.videoModel = self.videoArr.firstObject;
        _titleView.title = self.videoModel.title;
        [self addToolViewTimer];
        [self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
        
        _isReplay = NO;
    } else {
        
        if (!isPlay) {
            
            [self.player pause];
            self.link.paused = YES;
            [_activity stopAnimating];
            [self removeToolViewTimer];
        } else {
            
            [self.player play];
            self.link.paused = NO;
            [self addToolViewTimer];
        }
    }
}

- (void)toolView:(RHPlayerToolView *)toolView fullScreen:(BOOL)isFull {
    
    [self addToolViewTimer];
    //彈出全屏播放器
    if (isFull) {
        
        [_currentVC presentViewController:self.fullVC animated:NO completion:^{
            
            [_titleView showBackButton];
            [self.fullVC.view addSubview:self];
            self.center = self.fullVC.view.center;
            
            [UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
                
                self.frame = self.fullVC.view.bounds;
                _layerView.frame = self.bounds;
            } completion:nil];
        }];
    } else {
        
        [_titleView hideBackButton];
        [self.fullVC dismissViewControllerAnimated:NO completion:^{
            [_currentVC.view addSubview:self];
            
            [UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
                
                self.frame = _playerFrame;
                _layerView.frame = self.bounds;
            } completion:nil];
        }];
    }
}

#pragma mark - touch event

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    [self removeToolViewTimer];
    [self showOrHideBar];
}

- (void)showOrHideBar {
    
    [UIView animateWithDuration:0.25 animations:^{
        
        [_toolView mas_updateConstraints:^(MASConstraintMaker *make) {
            
            make.bottom.mas_equalTo(@(_isShowToolView ? 44 : 0));
        }];
        [_titleView mas_updateConstraints:^(MASConstraintMaker *make) {
           
            make.top.mas_equalTo(@(_isShowToolView ? -44 : 0));
        }];
        [self layoutIfNeeded];
    } completion:^(BOOL finished) {
        
        _isShowToolView = !_isShowToolView;
        if (_isShowToolView) {
            
            [self addToolViewTimer];
        }
    }];
    
}

- (void)dealloc {
    
    NSLog(@"player view dealloc");
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self removeObserverWithPlayerItem:self.playerItem];
}

#pragma mark - setter and getter

- (UIImageView *)coverImageView {
    
    if (!_coverImageView) {
        
        UIImageView * coverImageView = [[UIImageView alloc] init];
        coverImageView.contentMode = UIViewContentModeScaleAspectFill;
        coverImageView.clipsToBounds = YES;
        _coverImageView = coverImageView;
    }
    return _coverImageView;
}

- (RHFullViewController *)fullVC {
    
    if (!_fullVC) {
        
        RHFullViewController * fullVC = [[RHFullViewController alloc] init];
        _fullVC = fullVC;
    }
    return _fullVC;
}

- (RHPlayerLayerView *)layerView {
    
    if (!_layerView) {
        
        RHPlayerLayerView * layerView = [[RHPlayerLayerView alloc] init];
        _layerView = layerView;
    }
    return _layerView;
}

- (UIActivityIndicatorView *)activity {
    
    if (!_activity) {
        
        UIActivityIndicatorView * activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        activity.color = [UIColor redColor];
        // 指定進度輪中心點
        [activity setCenter:self.center];
        // 設置進度輪顯示類型
        [activity setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
        _activity = activity;
    }
    return _activity;
}

- (RHPlayerFailedView *)failedView {
    
    if (!_failedView) {
        
        RHPlayerFailedView * failedView = [[RHPlayerFailedView alloc] init];
        failedView.hidden = YES;
        _failedView = failedView;
    }
    return _failedView;
}

- (RHPlayerToolView *)toolView {
    
    if (!_toolView) {
        
        RHPlayerToolView * toolView = [[RHPlayerToolView alloc] init];
        toolView.delegate = self;
        [toolView.slider addTarget:self action:@selector(progressValueChange:) forControlEvents:UIControlEventValueChanged];
        _toolView = toolView;
    }
    return _toolView;
}

- (RHPlayerTitleView *)titleView {
    
    if (!_titleView) {
        
        RHPlayerTitleView * titleView = [[RHPlayerTitleView alloc] init];
        titleView.delegate = self;
        _titleView = titleView;
    }
    return _titleView;
}
@end

上面代碼比較多,在此給大家說一下核心的地方主要在于:
1、通過對AVPlayerItemloadedTimeRangesstatus兩個屬性的監聽來實現了播放緩沖進度和播放狀態的獲取。但是這兩個監聽不僅是添加了就完事了,在界面dealloc時一定要移除,否則會崩潰。
2、通過對播放器播放完成的通知監聽和保存視頻信息model的數組來實現視頻的順序播放。
3、通過定時器來實現播放器的標題欄和控制欄的動畫自動彈出和收起。
4、通過AVPlayerseekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler這個方法實現續播的功能。

下面我們來簡單看一下如何來使用這個定制好的RHPlayerView如下:

#import "PlayViewController.h"
#import "RHPlayerView.h"

@interface PlayViewController () <RHPlayerViewDelegate, UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) RHPlayerView * player;
@property (nonatomic, strong) UITableView * tableView;

@property (nonatomic, strong) NSMutableArray * dataArr;
@end

@implementation PlayViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self loadData];
    [self addSubviews];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    
    if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) {
        
        NSLog(@"pop pop pop pop pop");
        [_player stop];
    }
}

- (void)loadData {
    
    NSArray * titleArr = @[@"視頻一", @"視頻二", @"視頻三"];
    NSArray * urlArr = @[@"http://101.200.183.78:301/rm.mp4", @"http://101.200.183.78:301/rm.mp4", @"http://101.200.183.78:301/rm.mp4"];
    
    for (int i = 0; i < titleArr.count; i++) {
        
        RHVideoModel * model = [[RHVideoModel alloc] initWithVideoId:[NSString stringWithFormat:@"%03d", i + 1] title:titleArr[i] url:urlArr[i] currentTime:0];
        [self.dataArr addObject:model];
    }
    [self.player setVideoModels:self.dataArr playVideoId:@""];
    [self.tableView reloadData];
}

- (void)addSubviews {
    
    [self.view addSubview:self.player];
    [self.view addSubview:self.tableView];
    
    [self makeConstraintsForUI];
}

- (void)makeConstraintsForUI {
    
    [_tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        
        make.top.mas_equalTo(@(9 * Screen_Width / 16));
        make.left.mas_equalTo(@0);
        make.right.mas_equalTo(@0);
        make.bottom.mas_equalTo(@0);
    }];
}

#pragma mark - player view delegate

// 是否允許播放
- (BOOL)playerViewShouldPlay {
    
    return YES;
}
// 當前播放的
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel index:(NSInteger)index {
    
    
}
// 當前播放結束的
- (void)playerView:(RHPlayerView *)playView didPlayEndVideo:(RHVideoModel *)videoModel index:(NSInteger)index {
    
    
}
// 當前正在播放的  會調用多次  更新當前播放時間
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel playTime:(NSTimeInterval)playTime {
    
    
}
#pragma mark - tableView delegate

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    return _dataArr.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"Cell_ID"];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    if (indexPath.row < _dataArr.count) {
        
        RHVideoModel * model = _dataArr[indexPath.row];
        cell.textLabel.text = model.title;
    }
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    RHVideoModel * model = _dataArr[indexPath.row];
    [_player playVideoWithVideoId:model.videoId];
}

#pragma mark - setter and getter

- (UITableView *)tableView {
    
    if (!_tableView) {
        
        UITableView * tableView = [[UITableView alloc] init];
        tableView.dataSource = self;
        tableView.delegate = self;
        [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell_ID"];
        tableView.tableFooterView = [[UIView alloc] init];
        _tableView = tableView;
    }
    return _tableView;
}

- (RHPlayerView *)player {
    
    if (!_player) {
        
        _player = [[RHPlayerView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, 9 * Screen_Width / 16) currentVC:self];
        _player.delegate = self;
    }
    return _player;
}

- (NSMutableArray *)dataArr {
    
    if (!_dataArr) {
        
        _dataArr = [[NSMutableArray alloc] init];
    }
    return _dataArr;
}
@end

到此所有封裝結束,大家一定記得在界面pop的時候調用stop方法,要不會造成pop之后還有繼續播放的聲音(其實就是RHPlayerView沒有釋放,還一直存在)。

小編將此封裝單獨寫了demo,如果大家覺得想要看一下工具欄的封裝,可以去git上邊下載,地址如下:
https://github.com/guorenhao/AVPlayerDemo.git

最后,還是希望能夠幫助到有需要的猿友們,愿我們能夠共同成長進步,在開發的道路上越走越遠!謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容