前言:说到视频播放器,相信大家基本都能想到AVPlayer,使用AVPlayer简单的几行代码就可以实现本地和网络视频的播放。如果要实现稍微复杂点的功能,比如说增加进度条,全屏按钮等,如果把这些都写在
ViewController
里边的话会使
ViewController
显得代码比较冗杂。基于此,小编在使用AVPlayer时进行了封装,实现了播放进度时间展示、续播、缓冲进度条、进度条拖拽快进快退、多个视频顺序播放、全屏播放的功能。
原理:对AVPlayerItem
的loadedTimeRanges
和status
两个属性的监听实现缓冲进度和播放状态的获取;创建model
保存要播放的视频的信息并存储在数组中来实现顺序播放;对播放器的标题和工具栏进行封装来降低定制view
中的代码量,并使用代理传值进行回调。
先来看一下效果图
下面我们来正式开始进行封装:
首先,创建存储视频信息的
model
(大家可以根据自己需求进行修改)如下:
#import
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
上边,同时改变view
的frame
,新的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
并让AVPlayerLayer
的frame
跟随RHPlayerLayerView
的frame
的改变来改变,这样只需要对该RHPlayerLayerView
的frame
来进行修改即可。如下:
#import
#import
@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
#import
#import "RHVideoModel.h"
@protocol RHPlayerViewDelegate;
@interface RHPlayerView : UIView
@property (nonatomic, weak) id delegate;
/**
对象方法创建对象
@param frame 约束
@param controller 所在的控制器
@return 对象
*/
- (instancetype)initWithFrame:(CGRect)frame currentVC:(UIViewController *)controller;
/**
设置要播放的视频列表和要播放的视频
@param videoModels 存储视频model的数组
@param videoId 当前要播放的视频id
*/
- (void)setVideoModels:(NSArray *)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
// 是否可以播放
- (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
中的实现,由于添加的功能比较多,所以这里的代码比较多一些,希望大家能够耐心一些,其中的titleView
、toolView
、failedView
分别是定制的播放器上方的标题栏、下方的控制栏和播放失败显示的视图,大家在此可以暂时忽略这些,具体代码如下:
#import "RHPlayerView.h"
#import "RHFullViewController.h"
#import "RHPlayerTitleView.h"
#import "RHPlayerToolView.h"
#import "RHPlayerFailedView.h"
#import "RHPlayerLayerView.h"
@interface RHPlayerView ()
@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 *)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 *)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 *)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、通过对AVPlayerItem
的loadedTimeRanges
和status
两个属性的监听来实现了播放缓冲进度和播放状态的获取。但是这两个监听不仅是添加了就完事了,在界面dealloc
时一定要移除,否则会崩溃。
2、通过对播放器播放完成的通知监听和保存视频信息model
的数组来实现视频的顺序播放。
3、通过定时器来实现播放器的标题栏和控制栏的动画自动弹出和收起。
4、通过AVPlayer
的seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler
这个方法实现续播的功能。
下面我们来简单看一下如何来使用这个定制好的RHPlayerView
如下:
#import "PlayViewController.h"
#import "RHPlayerView.h"
@interface PlayViewController ()
@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
最后,还是希望能够帮助到有需要的猿友们,愿我们能够共同成长进步,在开发的道路上越走越远!谢谢!