iOS開(kāi)發(fā)-封裝AVPlayer播放網(wǎng)絡(luò)孝赫、本地視頻

AVPlayer.png

前言:說(shuō)到視頻播放器较木,相信大家基本都能想到AVPlayer,使用AVPlayer簡(jiǎn)單的幾行代碼就可以實(shí)現(xiàn)本地和網(wǎng)絡(luò)視頻的播放青柄。如果要實(shí)現(xiàn)稍微復(fù)雜點(diǎn)的功能伐债,比如說(shuō)增加進(jìn)度條预侯,全屏按鈕等,如果把這些都寫(xiě)在ViewController里邊的話會(huì)使ViewController顯得代碼比較冗雜峰锁∥冢基于此,小編在使用AVPlayer時(shí)進(jìn)行了封裝虹蒋,實(shí)現(xiàn)了播放進(jìn)度時(shí)間展示糜芳、續(xù)播、緩沖進(jìn)度條魄衅、進(jìn)度條拖拽快進(jìn)快退峭竣、多個(gè)視頻順序播放、全屏播放的功能徐绑。

原理:對(duì)AVPlayerItemloadedTimeRangesstatus兩個(gè)屬性的監(jiān)聽(tīng)實(shí)現(xiàn)緩沖進(jìn)度和播放狀態(tài)的獲刃巴浴莫辨;創(chuàng)建model保存要播放的視頻的信息并存儲(chǔ)在數(shù)組中來(lái)實(shí)現(xiàn)順序播放;對(duì)播放器的標(biāo)題和工具欄進(jìn)行封裝來(lái)降低定制view中的代碼量沮榜,并使用代理傳值進(jìn)行回調(diào)蟆融。

先來(lái)看一下效果圖

AVPlayer.gif

下面我們來(lái)正式開(kāi)始進(jìn)行封裝:
首先,創(chuàng)建存儲(chǔ)視頻信息的model(大家可以根據(jù)自己需求進(jìn)行修改)如下:

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, RHVideoPlayStyle) {
    
    RHVideoPlayStyleLocal = 0,       //播放本地視頻
    RHVideoPlayStyleNetwork,         //播放網(wǎng)絡(luò)視頻
    RHVideoPlayStyleNetworkSD,       //播放網(wǎng)絡(luò)標(biāo)清視頻
    RHVideoPlayStyleNetworkHD,       //播放網(wǎng)絡(luò)高清視頻
};
@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;

/**
 創(chuàng)建本地視頻模型

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

/**
 創(chuàng)建網(wǎng)絡(luò)視頻模型
 
 @param videoId     視頻ID
 @param title       標(biāo)題
 @param url         視頻地址
 @param currentTime 當(dāng)前播放時(shí)間
 @return            網(wǎng)絡(luò)視頻模型
 */
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title url:(NSString *)url currentTime:(NSTimeInterval)currentTime;

/**
 創(chuàng)建網(wǎng)絡(luò)視頻模型
 
 @param videoId     視頻ID
 @param title       標(biāo)題
 @param sdUrl       標(biāo)清地址
 @param hdUrl       高清地址
 @param currentTime 當(dāng)前播放時(shí)間
 @return            網(wǎng)絡(luò)視頻模型
 */
- (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

對(duì)此model的所有方法都已經(jīng)注釋?zhuān)诖瞬辉僮鲞^(guò)多詳解。

接下來(lái)給大家說(shuō)一下全屏的思想由境,我是在點(diǎn)擊全屏的時(shí)候棚亩,從當(dāng)前的ViewController彈出一個(gè)新的ViewController并且將播放的view從之前的ViewController移除并添加到新的ViewController上邊,同時(shí)改變viewframe虏杰,新的ViewController為橫屏狀態(tài)即可實(shí)現(xiàn)全屏效果讥蟆。先來(lái)看一下全屏的ViewController的實(shí)現(xiàn)只需要?jiǎng)?chuàng)建一個(gè)繼承于UIViewController的類(lèi),在.m中重寫(xiě)兩個(gè)方法如下:

#import "RHFullViewController.h"

@interface RHFullViewController ()

@end

@implementation RHFullViewController

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    
    return UIInterfaceOrientationMaskLandscape;
}

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

由于AVPlayer的播放顯示效果是在AVPlayerLayer上邊纺阔,所以小編寫(xiě)了一個(gè)RHPlayerLayerView來(lái)添加AVPlayerLayer并讓AVPlayerLayerframe跟隨RHPlayerLayerViewframe的改變來(lái)改變瘸彤,這樣只需要對(duì)該RHPlayerLayerViewframe來(lái)進(jìn)行修改即可。如下:

#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

對(duì)于播放器上邊的標(biāo)題和控制欄以及播放失敗顯示頁(yè)面的封裝在此就不多說(shuō)了笛钝,主要使用的是代理回調(diào)來(lái)傳值控制播放器的质况。

接下來(lái)低零,我們重點(diǎn)來(lái)說(shuō)對(duì)于AVPlayer的封裝:
首先創(chuàng)建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;

/**
 對(duì)象方法創(chuàng)建對(duì)象

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

/**
 設(shè)置要播放的視頻列表和要播放的視頻

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

/**
 設(shè)置覆蓋的圖片

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

/**
 點(diǎn)擊目錄要播放的視頻id

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

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

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

@end
@protocol RHPlayerViewDelegate <NSObject>

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

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

所有的方法都添加了注釋?zhuān)嘈糯蠹叶寄芤荒苛巳徽埽诖诵【幗o該view添加了代理掏婶,這樣可以在ViewController中控制播放器的播放并實(shí)時(shí)獲取播放進(jìn)度及播放的視頻信息。

接下來(lái)我們來(lái)看一下在RHPlayerView.m中的實(shí)現(xiàn)潭陪,由于添加的功能比較多雄妥,所以這里的代碼比較多一些,希望大家能夠耐心一些依溯,其中的titleView老厌、toolViewfailedView分別是定制的播放器上方的標(biāo)題欄黎炉、下方的控制欄和播放失敗顯示的視圖枝秤,大家在此可以暫時(shí)忽略這些,具體代碼如下:

#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;

// 當(dāng)前是否顯示控制條
@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;
}
// 設(shè)置覆蓋的圖片
- (void)setCoverImage:(NSString *)imageUrl {
    
    _coverImageView.hidden = NO;
    [_coverImageView sd_setImageWithURL:[NSURL URLWithString:imageUrl] placeholderImage:[UIImage imageNamed:@""]];
}

// 設(shè)置要播放的視頻列表和要播放的視頻
- (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;
}
// 點(diǎn)擊目錄要播放的視頻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];
    // 上部標(biāo)題欄
    [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 - 監(jiān)聽(tīng)視頻緩沖和加載狀態(tài)
//注冊(cè)觀察者監(jiān)聽(tīng)狀態(tài)和緩沖
- (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"];
    }
}

// 監(jiān)聽(tīng)變化方法
- (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

// 設(shè)置播放器
- (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;
    }
}

//切換當(dāng)前播放的內(nèi)容
- (void)replaceCurrentPlayerItemWithVideoModel:(RHVideoModel *)model {
    
    if (self.player) {
        
        if (model) {
            
            if (![self checkNetwork]) {
                
                return;
            }
            //由暫停狀態(tài)切換時(shí)候 開(kāi)啟定時(shí)器慷嗜,將暫停按鈕狀態(tài)設(shè)置為播放狀態(tài)
            self.link.paused = NO;
            _toolView.playSwitch.selected = YES;
            
            //移除當(dāng)前AVPlayerItem對(duì)"loadedTimeRanges"和"status"的監(jiān)聽(tīng)
            [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;
    }
}

//轉(zhuǎn)換時(shí)間成字符串
- (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;
}

// 獲取緩沖進(jìn)度
- (NSTimeInterval)availableDurationWithplayerItem:(AVPlayerItem *)playerItem {
    
    NSArray * loadedTimeRanges = [playerItem loadedTimeRanges];
    // 獲取緩沖區(qū)域
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];
    NSTimeInterval startSeconds = CMTimeGetSeconds(timeRange.start);
    NSTimeInterval durationSeconds = CMTimeGetSeconds(timeRange.duration);
    // 計(jì)算緩沖總進(jìn)度
    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 {
    
    // 這里做網(wǎng)絡(luò)監(jiān)測(cè)
    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
// 更新進(jìn)度條
- (void)updateSlider {
    
    NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
    NSTimeInterval total = CMTimeGetSeconds(self.player.currentItem.duration);
    //如果用戶在手動(dòng)滑動(dòng)滑塊淀弹,則不對(duì)滑塊的進(jìn)度進(jìn)行設(shè)置重繪
    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];
    }
    // 記錄當(dāng)前播放時(shí)間 用于區(qū)分是否卡頓顯示緩沖動(dòng)畫(huà)
    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];
        // 指定進(jìn)度輪中心點(diǎn)
        [activity setCenter:self.center];
        // 設(shè)置進(jìn)度輪顯示類(lèi)型
        [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

上面代碼比較多,在此給大家說(shuō)一下核心的地方主要在于:
1庆械、通過(guò)對(duì)AVPlayerItemloadedTimeRangesstatus兩個(gè)屬性的監(jiān)聽(tīng)來(lái)實(shí)現(xiàn)了播放緩沖進(jìn)度和播放狀態(tài)的獲取薇溃。但是這兩個(gè)監(jiān)聽(tīng)不僅是添加了就完事了,在界面dealloc時(shí)一定要移除缭乘,否則會(huì)崩潰沐序。
2、通過(guò)對(duì)播放器播放完成的通知監(jiān)聽(tīng)和保存視頻信息model的數(shù)組來(lái)實(shí)現(xiàn)視頻的順序播放堕绩。
3策幼、通過(guò)定時(shí)器來(lái)實(shí)現(xiàn)播放器的標(biāo)題欄和控制欄的動(dòng)畫(huà)自動(dòng)彈出和收起。
4奴紧、通過(guò)AVPlayerseekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler這個(gè)方法實(shí)現(xiàn)續(xù)播的功能特姐。

下面我們來(lái)簡(jiǎn)單看一下如何來(lái)使用這個(gè)定制好的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;
}
// 當(dāng)前播放的
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel index:(NSInteger)index {
    
    
}
// 當(dāng)前播放結(jié)束的
- (void)playerView:(RHPlayerView *)playView didPlayEndVideo:(RHVideoModel *)videoModel index:(NSInteger)index {
    
    
}
// 當(dāng)前正在播放的  會(huì)調(diào)用多次  更新當(dāng)前播放時(shí)間
- (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

到此所有封裝結(jié)束,大家一定記得在界面pop的時(shí)候調(diào)用stop方法绰寞,要不會(huì)造成pop之后還有繼續(xù)播放的聲音(其實(shí)就是RHPlayerView沒(méi)有釋放到逊,還一直存在)。

小編將此封裝單獨(dú)寫(xiě)了demo滤钱,如果大家覺(jué)得想要看一下工具欄的封裝觉壶,可以去git上邊下載,地址如下:
https://github.com/guorenhao/AVPlayerDemo.git

最后件缸,還是希望能夠幫助到有需要的猿友們铜靶,愿我們能夠共同成長(zhǎng)進(jìn)步,在開(kāi)發(fā)的道路上越走越遠(yuǎn)!謝謝争剿!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末已艰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蚕苇,更是在濱河造成了極大的恐慌哩掺,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涩笤,死亡現(xiàn)場(chǎng)離奇詭異嚼吞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蹬碧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)舱禽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人恩沽,你說(shuō)我怎么就攤上這事誊稚。” “怎么了罗心?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵里伯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我协屡,道長(zhǎng)俏脊,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任肤晓,我火速辦了婚禮,結(jié)果婚禮上认然,老公的妹妹穿的比我還像新娘补憾。我一直安慰自己,他們只是感情好卷员,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布盈匾。 她就那樣靜靜地躺著,像睡著了一般毕骡。 火紅的嫁衣襯著肌膚如雪削饵。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天未巫,我揣著相機(jī)與錄音窿撬,去河邊找鬼。 笑死叙凡,一個(gè)胖子當(dāng)著我的面吹牛劈伴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播握爷,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼跛璧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼严里!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起追城,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤刹碾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后座柱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體教硫,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年辆布,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瞬矩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锋玲,死狀恐怖景用,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惭蹂,我是刑警寧澤惦积,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站浮毯,受9級(jí)特大地震影響邮弹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜廷雅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一耗美、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧航缀,春花似錦商架、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至灿巧,卻和暖如春赶袄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抠藕。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工饿肺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人幢痘。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓唬格,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子购岗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容