前言
最近在項目中,遇到了視頻播放的需求,直接使用系統(tǒng)封裝的播放器太過于簡單,不能很好的滿足項目要求,于是花時間研究了一下,使用AVPlayer
來自定義播放器。
? ? 本視頻播放器主要自定義了帶緩沖顯示的進(jìn)度條,可以拖動調(diào)節(jié)視頻播放進(jìn)度的播放條,具有當(dāng)前播放時間和總時間的Label,全屏播放功能,定時消失的工具條。播放器已經(jīng)封裝到UIView
中,支持自動旋轉(zhuǎn)切換全屏,支持UITableView
。
主要功能
1.帶緩沖顯示的進(jìn)度條
在自定義的時候,主要是需要計算當(dāng)前進(jìn)度和監(jiān)聽緩沖的進(jìn)度,細(xì)節(jié)方面需要注意進(jìn)度顏色,進(jìn)度為0的時候要設(shè)置為透明色,緩沖完成的時候需要設(shè)置顏色,不然全屏切換就會導(dǎo)致緩沖完成的進(jìn)度條顏色消失。
- 自定義進(jìn)度條的代碼
#pragma mark - 創(chuàng)建UIProgressView
- (void)createProgress
{
CGFloat width;
if (_isFullScreen == NO)
{
width = self.frame.size.width;
}
else
{
width = self.frame.size.height;
}
_progress = [[UIProgressView alloc]init];
_progress.frame = CGRectMake(_startButton.right + Padding, 0, width - 80 - Padding - _startButton.right - Padding - Padding, Padding);
_progress.centerY = _bottomView.height/2.0;
//進(jìn)度條顏色
_progress.trackTintColor = ProgressColor;
// 計算緩沖進(jìn)度
NSTimeInterval timeInterval = [self availableDuration];
CMTime duration = _playerItem.duration;
CGFloat totalDuration = CMTimeGetSeconds(duration);
[_progress setProgress:timeInterval / totalDuration animated:NO];
CGFloat time = round(timeInterval);
CGFloat total = round(totalDuration);
//確保都是number
if (isnan(time) == 0 && isnan(total) == 0)
{
if (time == total)
{
//緩沖進(jìn)度顏色
_progress.progressTintColor = ProgressTintColor;
}
else
{
//緩沖進(jìn)度顏色
_progress.progressTintColor = [UIColor clearColor];
}
}
else
{
//緩沖進(jìn)度顏色
_progress.progressTintColor = [UIColor clearColor];
}
[_bottomView addSubview:_progress];
}
- 緩沖進(jìn)度計算和監(jiān)聽代碼
#pragma mark - 緩存條監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"loadedTimeRanges"])
{
// 計算緩沖進(jìn)度
NSTimeInterval timeInterval = [self availableDuration];
CMTime duration = _playerItem.duration;
CGFloat totalDuration = CMTimeGetSeconds(duration);
[_progress setProgress:timeInterval / totalDuration animated:NO];
//設(shè)置緩存進(jìn)度顏色
_progress.progressTintColor = ProgressTintColor;
}
}
2.可以拖動調(diào)節(jié)視頻播放進(jìn)度的播放條
這里主要需要注意的是創(chuàng)建的播放條需要比進(jìn)度條稍微長一點(diǎn),這樣才可以看到滑塊從開始到最后走完整個進(jìn)度條。播放條最好單獨(dú)新建一個繼承自UISlider
的控件,因為進(jìn)度條和播放條的大小很可能不能完美的重合在一起,這樣看起來就會有2條線條,很不美觀,內(nèi)部代碼將其默認(rèn)長度和起點(diǎn)重新布局。
- 播放條控件內(nèi)部代碼
? ? 這里重寫- (CGRect)trackRectForBounds:(CGRect)bounds
方法,才能改變播放條的大小。
// 控制slider的寬和高,這個方法才是真正的改變slider滑道的高的
- (CGRect)trackRectForBounds:(CGRect)bounds
{
[super trackRectForBounds:bounds];
return CGRectMake(-2, (self.frame.size.height - 2.6)/2.0, CGRectGetWidth(bounds) + 4, 2.6);
}
- 創(chuàng)建播放條代碼
#pragma mark - 創(chuàng)建UISlider
- (void)createSlider
{
_slider = [[Slider alloc]init];
_slider.frame = CGRectMake(_progress.x, 0, _progress.width, ViewHeight);
_slider.centerY = _bottomView.height/2.0;
[_bottomView addSubview:_slider];
//自定義滑塊大小
UIImage *image = [UIImage imageNamed:@"round"];
//改變滑塊大小
UIImage *tempImage = [image OriginImage:image scaleToSize:CGSizeMake( SliderSize, SliderSize)];
//改變滑塊顏色
UIImage *newImage = [tempImage imageWithTintColor:SliderColor];
[_slider setThumbImage:newImage forState:UIControlStateNormal];
//開始拖拽
[_slider addTarget:self
action:@selector(processSliderStartDragAction:)
forControlEvents:UIControlEventTouchDown];
//拖拽中
[_slider addTarget:self
action:@selector(sliderValueChangedAction:)
forControlEvents:UIControlEventValueChanged];
//結(jié)束拖拽
[_slider addTarget:self
action:@selector(processSliderEndDragAction:)
forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
//左邊顏色
_slider.minimumTrackTintColor = PlayFinishColor;
//右邊顏色
_slider.maximumTrackTintColor = [UIColor clearColor];
}
- 拖動播放條代碼
#pragma mark - 拖動進(jìn)度條
//開始
- (void)processSliderStartDragAction:(UISlider *)slider
{
//暫停
[self pausePlay];
[_timer invalidate];
}
//結(jié)束
- (void)processSliderEndDragAction:(UISlider *)slider
{
//繼續(xù)播放
[self playVideo];
_timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
target:self
selector:@selector(disappear)
userInfo:nil
repeats:NO];
}
//拖拽中
- (void)sliderValueChangedAction:(UISlider *)slider
{
//計算出拖動的當(dāng)前秒數(shù)
CGFloat total = (CGFloat)_playerItem.duration.value / _playerItem.duration.timescale;
NSInteger dragedSeconds = floorf(total * slider.value);
//轉(zhuǎn)換成CMTime才能給player來控制播放進(jìn)度
CMTime dragedCMTime = CMTimeMake(dragedSeconds, 1);
[_player seekToTime:dragedCMTime];
}
3.具有當(dāng)前播放時間和總時間的Label
創(chuàng)建時間顯示Label
的時候,我們需要創(chuàng)建一個定時器,每秒執(zhí)行一下代碼,來實現(xiàn)動態(tài)改變Label
上的時間顯示。
- Label創(chuàng)建代碼
#pragma mark - 創(chuàng)建播放時間
- (void)createCurrentTimeLabel
{
_currentTimeLabel = [[UILabel alloc]init];
_currentTimeLabel.frame = CGRectMake(0, 0, 80, Padding);
_currentTimeLabel.centerY = _progress.centerY;
_currentTimeLabel.right = _backView.right - Padding;
_currentTimeLabel.textColor = [UIColor whiteColor];
_currentTimeLabel.font = [UIFont systemFontOfSize:12];
_currentTimeLabel.text = @"00:00/00:00";
[_bottomView addSubview:_currentTimeLabel];
}
- Label上面定時器的定時事件
#pragma mark - 計時器事件
- (void)timeStack
{
if (_playerItem.duration.timescale != 0)
{
//總共時長
_slider.maximumValue = 1;
//當(dāng)前進(jìn)度
_slider.value = CMTimeGetSeconds([_playerItem currentTime]) / (_playerItem.duration.value / _playerItem.duration.timescale);
//當(dāng)前時長進(jìn)度progress
NSInteger proMin = (NSInteger)CMTimeGetSeconds([_player currentTime]) / 60;//當(dāng)前秒
NSInteger proSec = (NSInteger)CMTimeGetSeconds([_player currentTime]) % 60;//當(dāng)前分鐘
//duration 總時長
NSInteger durMin = (NSInteger)_playerItem.duration.value / _playerItem.duration.timescale / 60;//總秒
NSInteger durSec = (NSInteger)_playerItem.duration.value / _playerItem.duration.timescale % 60;//總分鐘
self.currentTimeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld / %02ld:%02ld", (long)proMin, proSec, durMin, durSec];
}
//開始播放停止轉(zhuǎn)子
if (_player.status == AVPlayerStatusReadyToPlay)
{
[_activity stopAnimating];
}
else
{
[_activity startAnimating];
}
}
4.全屏播放功能
上面都是一些基本功能,最重要的還是全屏功能的實現(xiàn)。全屏功能這里多說一下,由于我將播放器封裝到一個UIView
里邊,導(dǎo)致在做全屏的時候出現(xiàn)了一些問題。因為播放器被封裝起來了,全屏的時候,播放器的大小就很可能超出父類控件的大小范圍,造成了超出部分點(diǎn)擊事件無法獲取,最開始打算重寫父類-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法,但是想到這樣做就沒有達(dá)到封裝的目的,于是改變了一下思路,在全屏的時候,將播放器添加到Window
上,這樣播放器就不會超出父類的范圍大小,小屏的時候?qū)⒉シ牌鲝?code>Window上還原到原有的父類上。
- 全屏代碼
? ? 全屏的適配采用的是遍歷刪除原有控件,重新布局創(chuàng)建全屏控件的方法實現(xiàn)。
#pragma mark - 全屏按鈕響應(yīng)事件
- (void)maxAction:(UIButton *)button
{
if (_isFullScreen == NO)
{
[self fullScreenWithDirection:Letf];
}
else
{
[self originalscreen];
}
}
#pragma mark - 全屏
- (void)fullScreenWithDirection:(Direction)direction
{
//記錄播放器父類
_fatherView = self.superview;
_isFullScreen = YES;
//取消定時消失
[_timer invalidate];
[self setStatusBarHidden:YES];
//添加到Window上
[self.window addSubview:self];
if (direction == Letf)
{
[UIView animateWithDuration:0.25 animations:^{
self.transform = CGAffineTransformMakeRotation(M_PI / 2);
}];
}
else
{
[UIView animateWithDuration:0.25 animations:^{
self.transform = CGAffineTransformMakeRotation( - M_PI / 2);
}];
}
self.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
_playerLayer.frame = CGRectMake(0, 0, ScreenHeight, ScreenWidth);
//刪除原有控件
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
//創(chuàng)建全屏UI
[self creatUI];
}
#pragma mark - 原始大小
- (void)originalscreen
{
_isFullScreen = NO;
//取消定時消失
[_timer invalidate];
[self setStatusBarHidden:NO];
[UIView animateWithDuration:0.25 animations:^{
//還原大小
self.transform = CGAffineTransformMakeRotation(0);
}];
self.frame = _customFarme;
_playerLayer.frame = CGRectMake(0, 0, _customFarme.size.width, _customFarme.size.height);
//還原到原有父類上
[_fatherView addSubview:self];
//刪除
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
//創(chuàng)建小屏UI
[self creatUI];
}
- 創(chuàng)建播放器UI的代碼
#pragma mark - 創(chuàng)建播放器UI
- (void)creatUI
{
//最上面的View
_backView = [[UIView alloc]init];
_backView.frame = CGRectMake(0, _playerLayer.frame.origin.y, _playerLayer.frame.size.width, _playerLayer.frame.size.height);
_backView.backgroundColor = [UIColor clearColor];
[self addSubview:_backView];
//頂部View條
_topView = [[UIView alloc]init];
_topView.frame = CGRectMake(0, 0, _backView.width, ViewHeight);
_topView.backgroundColor = [UIColor colorWithRed:0.00000f green:0.00000f blue:0.00000f alpha:0.50000f];
[_backView addSubview:_topView];
//底部View條
_bottomView = [[UIView alloc] init];
_bottomView.frame = CGRectMake(0, _backView.height - ViewHeight, _backView.width, ViewHeight);
_bottomView.backgroundColor = [UIColor colorWithRed:0.00000f green:0.00000f blue:0.00000f alpha:0.50000f];
[_backView addSubview:_bottomView];
// 監(jiān)聽loadedTimeRanges屬性
[_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
//創(chuàng)建播放按鈕
[self createButton];
//創(chuàng)建進(jìn)度條
[self createProgress];
//創(chuàng)建播放條
[self createSlider];
//創(chuàng)建時間Label
[self createCurrentTimeLabel];
//創(chuàng)建返回按鈕
[self createBackButton];
//創(chuàng)建全屏按鈕
[self createMaxButton];
//創(chuàng)建點(diǎn)擊手勢
[self createGesture];
//計時器,循環(huán)執(zhí)行
[NSTimer scheduledTimerWithTimeInterval:1.0f
target:self
selector:@selector(timeStack)
userInfo:nil
repeats:YES];
//定時器,工具條消失
_timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
target:self
selector:@selector(disappear)
userInfo:nil
repeats:NO];
}
5.定時消失的工具條
如果工具條是顯示狀態(tài),不點(diǎn)擊視頻,默認(rèn)一段時間后,自動隱藏工具條,點(diǎn)擊視頻,直接隱藏工具條;如果工具條是隱藏狀態(tài),點(diǎn)擊視頻,就讓工具條顯示。功能說起來很簡單,最開始的時候,我使用GCD延遲代碼實現(xiàn),但是當(dāng)點(diǎn)擊讓工具條顯示,然后再次點(diǎn)擊讓工具條消失,多點(diǎn)幾下你會發(fā)現(xiàn)你的定時消失時間不對。這里我們需要注意的是,當(dāng)你再次點(diǎn)擊的時候需要取消上一次的延遲執(zhí)行代碼,才能夠讓下一次點(diǎn)擊的時候,延遲代碼正確執(zhí)行。這里采用定時器來實現(xiàn),因為定時器可以取消延遲執(zhí)行的代碼。
- 點(diǎn)擊視頻的代碼
#pragma mark - 輕拍方法
- (void)tapAction:(UITapGestureRecognizer *)tap
{
//取消定時消失
[_timer invalidate];
if (_backView.alpha == 1)
{
[UIView animateWithDuration:0.5 animations:^{
_backView.alpha = 0;
}];
}
else if (_backView.alpha == 0)
{
//添加定時消失
_timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
target:self
selector:@selector(disappear)
userInfo:nil
repeats:NO];
[UIView animateWithDuration:0.5 animations:^{
_backView.alpha = 1;
}];
}
}
接口與用法
這里是寫給懶人看的,對播放器做了一下簡單的封裝,留了幾個常用的接口,方便使用。
- 接口
/**視頻url*/
@property (nonatomic,strong) NSURL *url;
/**旋轉(zhuǎn)自動全屏,默認(rèn)Yes*/
@property (nonatomic,assign) BOOL autoFullScreen;
/**重復(fù)播放,默認(rèn)No*/
@property (nonatomic,assign) BOOL repeatPlay;
/**是否支持橫屏,默認(rèn)No*/
@property (nonatomic,assign) BOOL isLandscape;
/**播放*/
- (void)playVideo;
/**暫停*/
- (void)pausePlay;
/**返回按鈕回調(diào)方法*/
- (void)backButton:(BackButtonBlock) backButton;
/**播放完成回調(diào)*/
- (void)endPlay:(EndBolck) end;
/**銷毀播放器*/
- (void)destroyPlayer;
/**
根據(jù)播放器所在位置計算是否滑出屏幕,
@param tableView Cell所在tableView
@param cell 播放器所在Cell
@param beyond 滑出后的回調(diào)
*/
- (void)calculateWith:(UITableView *)tableView cell:(UITableViewCell *)cell beyond:(BeyondBlock) beyond;
- 使用方法
直接使用cocoapods導(dǎo)入,pod 'CLPlayer'
- 具體使用代碼
CLPlayerView *playerView = [[CLPlayerView alloc] initWithFrame:CGRectMake(0, 90, ScreenWidth, 300)];
[self.view addSubview:playerView];
//根據(jù)旋轉(zhuǎn)自動支持全屏,默認(rèn)支持
// playerView.autoFullScreen = NO;
//重復(fù)播放,默認(rèn)不播放
// playerView.repeatPlay = YES;
//如果播放器所在頁面支持橫屏,需要設(shè)置為Yes,不支持不需要設(shè)置(默認(rèn)不支持)
// playerView.isLandscape = YES;
//視頻地址
playerView.url = [NSURL URLWithString:@"http://wvideo.spriteapp.cn/video/2016/0215/56c1809735217_wpd.mp4"];
//播放
[playerView playVideo];
//返回按鈕點(diǎn)擊事件回調(diào)
[playerView backButton:^(UIButton *button) {
NSLog(@"返回按鈕被點(diǎn)擊");
}];
//播放完成回調(diào)
[playerView endPlay:^{
//銷毀播放器
[playerView destroyPlayer];
playerView = nil;
NSLog(@"播放完成");
}];
說明
UIImage+TintColor
是用來渲染圖片顏色的分類,由于缺少圖片資源,所以采用其他顏色圖片渲染成自己需要的顏色;UIImage+ScaleToSize
這個分類是用來改變圖片尺寸大小的,因為播放條中的滑塊不能直接改變大小,所以通過改變圖片尺寸大小來控制滑塊大小;UIView+SetRect
是用于適配的分類。
總結(jié)
在自定義播放器的時候,需要注意的細(xì)節(jié)太多,這里就不一一細(xì)說了,更多細(xì)節(jié)請看Demo,Demo中有很詳細(xì)的注釋。考慮到大部分APP不支持橫屏,播放器默認(rèn)是不支持橫屏的,如果需要支持橫屏(勾選了支持左右方向),創(chuàng)建播放器的時候,寫上這句代碼playerView.isLandscape = YES;
。
播放器效果圖
Demo地址
最近更新修改了很多地方的代碼,主要是使用Masonry來重構(gòu)了一下工具條,修復(fù)了一些bug,具體還請參考CLPlayer
如果喜歡,歡迎star。