前言:說(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ì)AVPlayerItem
的loadedTimeRanges
和status
兩個(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)看一下效果圖
下面我們來(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í)改變view
的frame
虏杰,新的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
并讓AVPlayerLayer
的frame
跟隨RHPlayerLayerView
的frame
的改變來(lái)改變瘸彤,這樣只需要對(duì)該RHPlayerLayerView
的frame
來(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
老厌、toolView
、failedView
分別是定制的播放器上方的標(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ì)AVPlayerItem
的loadedTimeRanges
和status
兩個(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ò)AVPlayer
的seekToTime:(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)!謝謝争剿!