前言:說到視頻播放器,相信大家基本都能想到AVPlayer,使用AVPlayer簡單的幾行代碼就可以實現本地和網絡視頻的播放。如果要實現稍微復雜點的功能,比如說增加進度條,全屏按鈕等,如果把這些都寫在
ViewController
里邊的話會使ViewController
顯得代碼比較冗雜。基于此,小編在使用AVPlayer時進行了封裝,實現了播放進度時間展示、續播、緩沖進度條、進度條拖拽快進快退、多個視頻順序播放、全屏播放的功能。
原理:對AVPlayerItem
的loadedTimeRanges
和status
兩個屬性的監聽實現緩沖進度和播放狀態的獲取;創建model
保存要播放的視頻的信息并存儲在數組中來實現順序播放;對播放器的標題和工具欄進行封裝來降低定制view
中的代碼量,并使用代理傳值進行回調。
先來看一下效果圖
下面我們來正式開始進行封裝:
首先,創建存儲視頻信息的
model
(大家可以根據自己需求進行修改)如下:
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, RHVideoPlayStyle) {
RHVideoPlayStyleLocal = 0, //播放本地視頻
RHVideoPlayStyleNetwork, //播放網絡視頻
RHVideoPlayStyleNetworkSD, //播放網絡標清視頻
RHVideoPlayStyleNetworkHD, //播放網絡高清視頻
};
@interface RHVideoModel : NSObject
@property (nonatomic, copy, readonly) NSString * videoId;
@property (nonatomic, copy, readonly) NSString * title;
@property (nonatomic, strong, readonly) NSURL * url;
@property (nonatomic, assign) RHVideoPlayStyle style;
@property (nonatomic, assign) NSTimeInterval currentTime;
/**
創建本地視頻模型
@param videoId 視頻ID
@param title 標題
@param videoPath 播放文件路徑
@param currentTime 當前播放時間
@return 本地視頻模型
*/
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title videoPath:(NSString *)videoPath currentTime:(NSTimeInterval)currentTime;
/**
創建網絡視頻模型
@param videoId 視頻ID
@param title 標題
@param url 視頻地址
@param currentTime 當前播放時間
@return 網絡視頻模型
*/
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title url:(NSString *)url currentTime:(NSTimeInterval)currentTime;
/**
創建網絡視頻模型
@param videoId 視頻ID
@param title 標題
@param sdUrl 標清地址
@param hdUrl 高清地址
@param currentTime 當前播放時間
@return 網絡視頻模型
*/
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title sdUrl:(NSString *)sdUrl hdUrl:(NSString *)hdUrl currentTime:(NSTimeInterval)currentTime;
@end
#import "RHVideoModel.h"
@interface RHVideoModel ()
@property (nonatomic, copy) NSString * sdUrl;
@property (nonatomic, copy) NSString * hdUrl;
@end
@implementation RHVideoModel
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title videoPath:(NSString *)videoPath currentTime:(NSTimeInterval)currentTime {
self = [super init];
if (self) {
_videoId = [videoId copy];
_title = [title copy];
_currentTime = currentTime;
_url = [[NSURL fileURLWithPath:videoPath] copy];
_style = RHVideoPlayStyleLocal;
}
return self;
}
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title url:(NSString *)url currentTime:(NSTimeInterval)currentTime {
self = [super init];
if (self) {
_videoId = [videoId copy];
_title = [title copy];
_currentTime = currentTime;
_url = [[NSURL URLWithString:url] copy];
_style = RHVideoPlayStyleNetwork;
}
return self;
}
- (instancetype)initWithVideoId:(NSString *)videoId title:(NSString *)title sdUrl:(NSString *)sdUrl hdUrl:(NSString *)hdUrl currentTime:(NSTimeInterval)currentTime {
self = [super init];
if (self) {
_videoId = [videoId copy];
_title = [title copy];
_currentTime = currentTime;
_sdUrl = [sdUrl copy];
_hdUrl = [hdUrl copy];
self.style = RHVideoPlayStyleNetworkHD;
}
return self;
}
- (void)setStyle:(RHVideoPlayStyle)style {
_style = style;
if (_style == RHVideoPlayStyleNetworkSD) {
_url = [[NSURL URLWithString:_sdUrl] copy];
NSLog(@"%@", _sdUrl);
} else if (_style == RHVideoPlayStyleNetworkHD) {
_url = [[NSURL URLWithString:_hdUrl] copy];
NSLog(@"%@", _hdUrl);
}
}
@end
對此model
的所有方法都已經注釋,在此不再做過多詳解。
接下來給大家說一下全屏的思想,我是在點擊全屏的時候,從當前的ViewController
彈出一個新的ViewController
并且將播放的view
從之前的ViewController
移除并添加到新的ViewController
上邊,同時改變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 <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface RHPlayerLayerView : UIView
- (void)addPlayerLayer:(AVPlayerLayer *)playerLayer;
@end
#import "RHPlayerLayerView.h"
@interface RHPlayerLayerView ()
@property (nonatomic, strong) AVPlayerLayer * playerLayer;
@end
@implementation RHPlayerLayerView
- (void)addPlayerLayer:(AVPlayerLayer *)playerLayer {
_playerLayer = playerLayer;
playerLayer.backgroundColor = [UIColor blackColor].CGColor;
_playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
_playerLayer.contentsScale = [UIScreen mainScreen].scale;
[self.layer addSublayer:_playerLayer];
}
- (void)layoutSublayersOfLayer:(CALayer *)layer {
[super layoutSublayersOfLayer:layer];
_playerLayer.frame = self.bounds;
}
@end
對于播放器上邊的標題和控制欄以及播放失敗顯示頁面的封裝在此就不多說了,主要使用的是代理回調來傳值控制播放器的。
接下來,我們重點來說對于AVPlayer
的封裝:
首先創建RHPlayerView
繼承于UIView
,在RHPlayerView.h
中定義方法如下:
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "RHVideoModel.h"
@protocol RHPlayerViewDelegate;
@interface RHPlayerView : UIView
@property (nonatomic, weak) id<RHPlayerViewDelegate> delegate;
/**
對象方法創建對象
@param frame 約束
@param controller 所在的控制器
@return 對象
*/
- (instancetype)initWithFrame:(CGRect)frame currentVC:(UIViewController *)controller;
/**
設置要播放的視頻列表和要播放的視頻
@param videoModels 存儲視頻model的數組
@param videoId 當前要播放的視頻id
*/
- (void)setVideoModels:(NSArray<RHVideoModel *> *)videoModels playVideoId:(NSString *)videoId;
/**
設置覆蓋的圖片
@param imageUrl 覆蓋的圖片url
*/
- (void)setCoverImage:(NSString *)imageUrl;
/**
點擊目錄要播放的視頻id
@param videoId 要不放的視頻id
*/
- (void)playVideoWithVideoId:(NSString *)videoId;
/**
暫停
*/
- (void)pause;
/**
停止
*/
- (void)stop;
@end
@protocol RHPlayerViewDelegate <NSObject>
// 是否可以播放
- (BOOL)playerViewShouldPlay;
@optional
// 播放結束
- (void)playerView:(RHPlayerView *)playView didPlayEndVideo:(RHVideoModel *)videoModel index:(NSInteger)index;
// 開始播放
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel index:(NSInteger)index;
// 播放中
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel playTime:(NSTimeInterval)playTime;
@end
所有的方法都添加了注釋,相信大家都能一目了然,在此小編給該view
添加了代理,這樣可以在ViewController
中控制播放器的播放并實時獲取播放進度及播放的視頻信息。
接下來我們來看一下在RHPlayerView.m
中的實現,由于添加的功能比較多,所以這里的代碼比較多一些,希望大家能夠耐心一些,其中的titleView
、toolView
、failedView
分別是定制的播放器上方的標題欄、下方的控制欄和播放失敗顯示的視圖,大家在此可以暫時忽略這些,具體代碼如下:
#import "RHPlayerView.h"
#import "RHFullViewController.h"
#import "RHPlayerTitleView.h"
#import "RHPlayerToolView.h"
#import "RHPlayerFailedView.h"
#import "RHPlayerLayerView.h"
@interface RHPlayerView () <RHPlayerToolViewDelegate, RHPlayerTitleViewDelegate, RHPlayerFailedViewDelegate>
@property (nonatomic, strong) AVPlayer * player;
@property (nonatomic, strong) AVPlayerItem * playerItem;
@property (nonatomic, strong) AVPlayerLayer * playerLayer;
@property (nonatomic, strong) RHFullViewController * fullVC;
@property (nonatomic, weak) UIViewController * currentVC;
@property (nonatomic, strong) RHPlayerTitleView * titleView;
@property (nonatomic, strong) RHPlayerToolView * toolView;
@property (nonatomic, strong) RHPlayerFailedView * failedView;
@property (nonatomic, strong) RHPlayerLayerView * layerView;
@property (nonatomic, strong) UIActivityIndicatorView * activity;
@property (nonatomic, strong) UIImageView * coverImageView;
@property (nonatomic, strong) CADisplayLink * link;
@property (nonatomic, assign) NSTimeInterval lastTime;
@property (nonatomic, strong) NSTimer * toolViewShowTimer;
@property (nonatomic, assign) NSTimeInterval toolViewShowTime;
// 當前是否顯示控制條
@property (nonatomic, assign) BOOL isShowToolView;
// 是否第一次播放
@property (nonatomic, assign) BOOL isFirstPlay;
// 是否重播
@property (nonatomic, assign) BOOL isReplay;
@property (nonatomic, strong) NSArray * videoArr;
@property (nonatomic, strong) RHVideoModel * videoModel;
@property (nonatomic) CGRect playerFrame;
@end
@implementation RHPlayerView
#pragma mark - public
// 初始化方法
- (instancetype)initWithFrame:(CGRect)frame currentVC:(UIViewController *)controller {
self = [super initWithFrame:frame];
if (self) {
self.clipsToBounds = YES;
self.backgroundColor = [UIColor blackColor];
self.currentVC = controller;
_isShowToolView = YES;
_isFirstPlay = YES;
_isReplay = NO;
_playerFrame = frame;
[self addSubviews];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayEnd) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}
return self;
}
// 設置覆蓋的圖片
- (void)setCoverImage:(NSString *)imageUrl {
_coverImageView.hidden = NO;
[_coverImageView sd_setImageWithURL:[NSURL URLWithString:imageUrl] placeholderImage:[UIImage imageNamed:@""]];
}
// 設置要播放的視頻列表和要播放的視頻
- (void)setVideoModels:(NSArray<RHVideoModel *> *)videoModels playVideoId:(NSString *)videoId {
self.videoArr = [NSArray arrayWithArray:videoModels];
if (videoId.length > 0) {
for (RHVideoModel * model in self.videoArr) {
if ([model.videoId isEqualToString:videoId]) {
NSInteger index = [self.videoArr indexOfObject:model];
self.videoModel = self.videoArr[index];
break;
}
}
} else {
self.videoModel = self.videoArr.firstObject;
}
_titleView.title = self.videoModel.title;
_isFirstPlay = YES;
}
// 點擊目錄要播放的視頻id
- (void)playVideoWithVideoId:(NSString *)videoId {
if (![self.delegate respondsToSelector:@selector(playerViewShouldPlay)]) {
return;
}
[self.delegate playerViewShouldPlay];
for (RHVideoModel * model in self.videoArr) {
if ([model.videoId isEqualToString:videoId]) {
NSInteger index = [self.videoArr indexOfObject:model];
self.videoModel = self.videoArr[index];
break;
}
}
_titleView.title = self.videoModel.title;
if (_isFirstPlay) {
_coverImageView.hidden = YES;
[self setPlayer];
[self addToolViewTimer];
_isFirstPlay = NO;
} else {
[self.player pause];
[self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
[self addToolViewTimer];
}
}
// 暫停
- (void)pause {
[self.player pause];
self.link.paused = YES;
_toolView.playSwitch.selected = NO;
[self removeToolViewTimer];
}
// 停止
- (void)stop {
[self.player pause];
[self.link invalidate];
_toolView.playSwitch.selected = NO;
[self removeToolViewTimer];
}
#pragma mark - add subviews and make constraints
- (void)addSubviews {
// 播放的layerView
[self addSubview:self.layerView];
// 菊花
[self addSubview:self.activity];
// 加載失敗
[self addSubview:self.failedView];
// 覆蓋的圖片
[self addSubview:self.coverImageView];
// 下部工具欄
[self addSubview:self.toolView];
// 上部標題欄
[self addSubview:self.titleView];
// 添加約束
[self makeConstraintsForUI];
}
- (void)makeConstraintsForUI {
[_layerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@0);
make.left.mas_equalTo(@0);
make.right.mas_equalTo(@0);
make.bottom.mas_equalTo(@0);
}];
[_toolView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(@0);
make.left.mas_equalTo(@0);
make.right.mas_equalTo(@0);
make.height.mas_equalTo(@44);
}];
[_titleView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@0);
make.left.mas_equalTo(@0);
make.right.mas_equalTo(@0);
make.height.mas_equalTo(@44);
}];
[_activity mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(30, 30));
make.centerX.mas_equalTo(self.mas_centerX);
make.centerY.mas_equalTo(self.mas_centerY);
}];
[_failedView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@0);
make.left.mas_equalTo(@0);
make.right.mas_equalTo(@0);
make.bottom.mas_equalTo(@0);
}];
[_coverImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@0);
make.left.mas_equalTo(@0);
make.right.mas_equalTo(@0);
make.bottom.mas_equalTo(@0);
}];
}
- (void)layoutSubviews {
[self.superview bringSubviewToFront:self];
}
#pragma mark - notification
// 視頻播放完成通知
- (void)videoPlayEnd {
NSLog(@"播放完成");
_toolView.playSwitch.selected = NO;
[UIView animateWithDuration:0.25 animations:^{
[_toolView mas_updateConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(@0);
}];
[_titleView mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@0);
}];
[self layoutIfNeeded];
} completion:^(BOOL finished) {
_isShowToolView = YES;
}];
self.videoModel.currentTime = 0;
NSInteger index = [self.videoArr indexOfObject:self.videoModel];
if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayEndVideo:index:)]) {
[self.delegate playerView:self didPlayEndVideo:self.videoModel index:index];
}
if (index != self.videoArr.count - 1) {
[self.player pause];
self.videoModel = self.videoArr[index + 1];
_titleView.title = self.videoModel.title;
[self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
[self addToolViewTimer];
} else {
_isReplay = YES;
[self.player pause];
self.link.paused = YES;
[self removeToolViewTimer];
_coverImageView.hidden = NO;
_toolView.slider.sliderPercent = 0;
_toolView.slider.enabled = NO;
[_activity stopAnimating];
}
}
#pragma mark - 監聽視頻緩沖和加載狀態
//注冊觀察者監聽狀態和緩沖
- (void)addObserverWithPlayerItem:(AVPlayerItem *)playerItem {
if (playerItem) {
[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
}
}
//移除觀察者
- (void)removeObserverWithPlayerItem:(AVPlayerItem *)playerItem {
if (playerItem) {
[playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
[playerItem removeObserver:self forKeyPath:@"status"];
}
}
// 監聽變化方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
AVPlayerItem * playerItem = (AVPlayerItem *)object;
if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
NSTimeInterval loadedTime = [self availableDurationWithplayerItem:playerItem];
NSTimeInterval totalTime = CMTimeGetSeconds(playerItem.duration);
if (!_toolView.slider.isSliding) {
_toolView.slider.progressPercent = loadedTime/totalTime;
}
} else if ([keyPath isEqualToString:@"status"]) {
if (playerItem.status == AVPlayerItemStatusReadyToPlay) {
NSLog(@"playerItem is ready");
[self.player play];
self.link.paused = NO;
CMTime seekTime = CMTimeMake(self.videoModel.currentTime, 1);
[self.player seekToTime:seekTime completionHandler:^(BOOL finished) {
if (finished) {
NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
_toolView.currentTimeLabel.text = [self convertTimeToString:current];
}
}];
_toolView.slider.enabled = YES;
_toolView.playSwitch.enabled = YES;
_toolView.playSwitch.selected = YES;
} else{
NSLog(@"load break");
self.failedView.hidden = NO;
}
}
}
#pragma mark - private
// 設置播放器
- (void)setPlayer {
if (self.videoModel) {
if (self.videoModel.url) {
if (![self checkNetwork]) {
return;
}
AVPlayerItem * item = [AVPlayerItem playerItemWithURL:self.videoModel.url];
self.playerItem = item;
[self addObserverWithPlayerItem:self.playerItem];
if (self.player) {
[self.player replaceCurrentItemWithPlayerItem:self.playerItem];
} else {
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
}
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[_layerView addPlayerLayer:self.playerLayer];
NSInteger index = [self.videoArr indexOfObject:self.videoModel];
if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayVideo:index:)]) {
[self.delegate playerView:self didPlayVideo:self.videoModel index:index];
}
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateSlider)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
} else {
_failedView.hidden = NO;
}
} else {
_failedView.hidden = NO;
}
}
//切換當前播放的內容
- (void)replaceCurrentPlayerItemWithVideoModel:(RHVideoModel *)model {
if (self.player) {
if (model) {
if (![self checkNetwork]) {
return;
}
//由暫停狀態切換時候 開啟定時器,將暫停按鈕狀態設置為播放狀態
self.link.paused = NO;
_toolView.playSwitch.selected = YES;
//移除當前AVPlayerItem對"loadedTimeRanges"和"status"的監聽
[self removeObserverWithPlayerItem:self.playerItem];
if (model.url) {
AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:model.url];
self.playerItem = playerItem;
[self addObserverWithPlayerItem:self.playerItem];
//更換播放的AVPlayerItem
[self.player replaceCurrentItemWithPlayerItem:self.playerItem];
NSInteger index = [self.videoArr indexOfObject:self.videoModel];
if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayVideo:index:)]) {
[self.delegate playerView:self didPlayVideo:self.videoModel index:index];
}
_toolView.playSwitch.enabled = NO;
_toolView.slider.enabled = NO;
} else {
_toolView.playSwitch.selected = NO;
_failedView.hidden = NO;
}
} else {
_toolView.playSwitch.selected = NO;
_failedView.hidden = NO;
}
} else {
_toolView.playSwitch.selected = NO;
_failedView.hidden = NO;
}
}
//轉換時間成字符串
- (NSString *)convertTimeToString:(NSTimeInterval)time {
if (time <= 0) {
return @"00:00";
}
int minute = time / 60;
int second = (int)time % 60;
NSString * timeStr;
if (minute >= 100) {
timeStr = [NSString stringWithFormat:@"%d:%02d", minute, second];
}else {
timeStr = [NSString stringWithFormat:@"%02d:%02d", minute, second];
}
return timeStr;
}
// 獲取緩沖進度
- (NSTimeInterval)availableDurationWithplayerItem:(AVPlayerItem *)playerItem {
NSArray * loadedTimeRanges = [playerItem loadedTimeRanges];
// 獲取緩沖區域
CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];
NSTimeInterval startSeconds = CMTimeGetSeconds(timeRange.start);
NSTimeInterval durationSeconds = CMTimeGetSeconds(timeRange.duration);
// 計算緩沖總進度
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
- (void)addToolViewTimer {
[self removeToolViewTimer];
_toolViewShowTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(updateToolViewShowTime) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_toolViewShowTimer forMode:NSRunLoopCommonModes];
}
- (void)removeToolViewTimer {
[_toolViewShowTimer invalidate];
_toolViewShowTimer = nil;
_toolViewShowTime = 0;
}
- (BOOL)checkNetwork {
// 這里做網絡監測
return YES;
}
#pragma mark - slider event
- (void)progressValueChange:(RHProgressSlider *)slider {
[self addToolViewTimer];
if (self.player.status == AVPlayerStatusReadyToPlay) {
NSTimeInterval duration = slider.sliderPercent * CMTimeGetSeconds(self.player.currentItem.duration);
CMTime seekTime = CMTimeMake(duration, 1);
[self.player seekToTime:seekTime completionHandler:^(BOOL finished) {
if (finished) {
NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
_toolView.currentTimeLabel.text = [self convertTimeToString:current];
}
}];
}
}
#pragma mark - timer event
// 更新進度條
- (void)updateSlider {
NSTimeInterval current = CMTimeGetSeconds(self.player.currentTime);
NSTimeInterval total = CMTimeGetSeconds(self.player.currentItem.duration);
//如果用戶在手動滑動滑塊,則不對滑塊的進度進行設置重繪
if (!_toolView.slider.isSliding) {
_toolView.slider.sliderPercent = current/total;
}
if (current != self.lastTime) {
[_activity stopAnimating];
_toolView.currentTimeLabel.text = [self convertTimeToString:current];
_toolView.totleTimeLabel.text = isnan(total) ? @"00:00" : [self convertTimeToString:total];
if (self.delegate && [self.delegate respondsToSelector:@selector(playerView:didPlayVideo:playTime:)]) {
[self.delegate playerView:self didPlayVideo:self.videoModel playTime:current];
}
}else{
[_activity startAnimating];
}
// 記錄當前播放時間 用于區分是否卡頓顯示緩沖動畫
self.lastTime = current;
}
- (void)updateToolViewShowTime {
_toolViewShowTime++;
if (_toolViewShowTime == 5) {
[self removeToolViewTimer];
_toolViewShowTime = 0;
[self showOrHideBar];
}
}
#pragma mark - failedView delegate
// 重新播放
- (void)failedViewDidReplay:(RHPlayerFailedView *)failedView {
_failedView.hidden = YES;
[self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
}
#pragma mark - titleView delegate
- (void)titleViewDidExitFullScreen:(RHPlayerTitleView *)titleView {
[_toolView exitFullScreen];
}
#pragma mark - toolView delegate
- (void)toolView:(RHPlayerToolView *)toolView playSwitch:(BOOL)isPlay {
if (_isFirstPlay) {
if (![self.delegate playerViewShouldPlay]) {
_toolView.playSwitch.selected = !_toolView.playSwitch.selected;
return;
}
_coverImageView.hidden = YES;
if (!self.videoModel.videoId) {
_coverImageView.hidden = NO;
_toolView.playSwitch.selected = !_toolView.playSwitch.selected;
return;
}
[self setPlayer];
[self addToolViewTimer];
_isFirstPlay = NO;
} else if (_isReplay) {
_coverImageView.hidden = YES;
self.videoModel = self.videoArr.firstObject;
_titleView.title = self.videoModel.title;
[self addToolViewTimer];
[self replaceCurrentPlayerItemWithVideoModel:self.videoModel];
_isReplay = NO;
} else {
if (!isPlay) {
[self.player pause];
self.link.paused = YES;
[_activity stopAnimating];
[self removeToolViewTimer];
} else {
[self.player play];
self.link.paused = NO;
[self addToolViewTimer];
}
}
}
- (void)toolView:(RHPlayerToolView *)toolView fullScreen:(BOOL)isFull {
[self addToolViewTimer];
//彈出全屏播放器
if (isFull) {
[_currentVC presentViewController:self.fullVC animated:NO completion:^{
[_titleView showBackButton];
[self.fullVC.view addSubview:self];
self.center = self.fullVC.view.center;
[UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
self.frame = self.fullVC.view.bounds;
_layerView.frame = self.bounds;
} completion:nil];
}];
} else {
[_titleView hideBackButton];
[self.fullVC dismissViewControllerAnimated:NO completion:^{
[_currentVC.view addSubview:self];
[UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
self.frame = _playerFrame;
_layerView.frame = self.bounds;
} completion:nil];
}];
}
}
#pragma mark - touch event
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self removeToolViewTimer];
[self showOrHideBar];
}
- (void)showOrHideBar {
[UIView animateWithDuration:0.25 animations:^{
[_toolView mas_updateConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(@(_isShowToolView ? 44 : 0));
}];
[_titleView mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@(_isShowToolView ? -44 : 0));
}];
[self layoutIfNeeded];
} completion:^(BOOL finished) {
_isShowToolView = !_isShowToolView;
if (_isShowToolView) {
[self addToolViewTimer];
}
}];
}
- (void)dealloc {
NSLog(@"player view dealloc");
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self removeObserverWithPlayerItem:self.playerItem];
}
#pragma mark - setter and getter
- (UIImageView *)coverImageView {
if (!_coverImageView) {
UIImageView * coverImageView = [[UIImageView alloc] init];
coverImageView.contentMode = UIViewContentModeScaleAspectFill;
coverImageView.clipsToBounds = YES;
_coverImageView = coverImageView;
}
return _coverImageView;
}
- (RHFullViewController *)fullVC {
if (!_fullVC) {
RHFullViewController * fullVC = [[RHFullViewController alloc] init];
_fullVC = fullVC;
}
return _fullVC;
}
- (RHPlayerLayerView *)layerView {
if (!_layerView) {
RHPlayerLayerView * layerView = [[RHPlayerLayerView alloc] init];
_layerView = layerView;
}
return _layerView;
}
- (UIActivityIndicatorView *)activity {
if (!_activity) {
UIActivityIndicatorView * activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
activity.color = [UIColor redColor];
// 指定進度輪中心點
[activity setCenter:self.center];
// 設置進度輪顯示類型
[activity setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
_activity = activity;
}
return _activity;
}
- (RHPlayerFailedView *)failedView {
if (!_failedView) {
RHPlayerFailedView * failedView = [[RHPlayerFailedView alloc] init];
failedView.hidden = YES;
_failedView = failedView;
}
return _failedView;
}
- (RHPlayerToolView *)toolView {
if (!_toolView) {
RHPlayerToolView * toolView = [[RHPlayerToolView alloc] init];
toolView.delegate = self;
[toolView.slider addTarget:self action:@selector(progressValueChange:) forControlEvents:UIControlEventValueChanged];
_toolView = toolView;
}
return _toolView;
}
- (RHPlayerTitleView *)titleView {
if (!_titleView) {
RHPlayerTitleView * titleView = [[RHPlayerTitleView alloc] init];
titleView.delegate = self;
_titleView = titleView;
}
return _titleView;
}
@end
上面代碼比較多,在此給大家說一下核心的地方主要在于:
1、通過對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 () <RHPlayerViewDelegate, UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) RHPlayerView * player;
@property (nonatomic, strong) UITableView * tableView;
@property (nonatomic, strong) NSMutableArray * dataArr;
@end
@implementation PlayViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self loadData];
[self addSubviews];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) {
NSLog(@"pop pop pop pop pop");
[_player stop];
}
}
- (void)loadData {
NSArray * titleArr = @[@"視頻一", @"視頻二", @"視頻三"];
NSArray * urlArr = @[@"http://101.200.183.78:301/rm.mp4", @"http://101.200.183.78:301/rm.mp4", @"http://101.200.183.78:301/rm.mp4"];
for (int i = 0; i < titleArr.count; i++) {
RHVideoModel * model = [[RHVideoModel alloc] initWithVideoId:[NSString stringWithFormat:@"%03d", i + 1] title:titleArr[i] url:urlArr[i] currentTime:0];
[self.dataArr addObject:model];
}
[self.player setVideoModels:self.dataArr playVideoId:@""];
[self.tableView reloadData];
}
- (void)addSubviews {
[self.view addSubview:self.player];
[self.view addSubview:self.tableView];
[self makeConstraintsForUI];
}
- (void)makeConstraintsForUI {
[_tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@(9 * Screen_Width / 16));
make.left.mas_equalTo(@0);
make.right.mas_equalTo(@0);
make.bottom.mas_equalTo(@0);
}];
}
#pragma mark - player view delegate
// 是否允許播放
- (BOOL)playerViewShouldPlay {
return YES;
}
// 當前播放的
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel index:(NSInteger)index {
}
// 當前播放結束的
- (void)playerView:(RHPlayerView *)playView didPlayEndVideo:(RHVideoModel *)videoModel index:(NSInteger)index {
}
// 當前正在播放的 會調用多次 更新當前播放時間
- (void)playerView:(RHPlayerView *)playView didPlayVideo:(RHVideoModel *)videoModel playTime:(NSTimeInterval)playTime {
}
#pragma mark - tableView delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataArr.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"Cell_ID"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (indexPath.row < _dataArr.count) {
RHVideoModel * model = _dataArr[indexPath.row];
cell.textLabel.text = model.title;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
RHVideoModel * model = _dataArr[indexPath.row];
[_player playVideoWithVideoId:model.videoId];
}
#pragma mark - setter and getter
- (UITableView *)tableView {
if (!_tableView) {
UITableView * tableView = [[UITableView alloc] init];
tableView.dataSource = self;
tableView.delegate = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell_ID"];
tableView.tableFooterView = [[UIView alloc] init];
_tableView = tableView;
}
return _tableView;
}
- (RHPlayerView *)player {
if (!_player) {
_player = [[RHPlayerView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, 9 * Screen_Width / 16) currentVC:self];
_player.delegate = self;
}
return _player;
}
- (NSMutableArray *)dataArr {
if (!_dataArr) {
_dataArr = [[NSMutableArray alloc] init];
}
return _dataArr;
}
@end
到此所有封裝結束,大家一定記得在界面pop
的時候調用stop
方法,要不會造成pop
之后還有繼續播放的聲音(其實就是RHPlayerView
沒有釋放,還一直存在)。
小編將此封裝單獨寫了demo,如果大家覺得想要看一下工具欄的封裝,可以去git上邊下載,地址如下:
https://github.com/guorenhao/AVPlayerDemo.git
最后,還是希望能夠幫助到有需要的猿友們,愿我們能夠共同成長進步,在開發的道路上越走越遠!謝謝!