前言
最近在專案中,遇到了視訊播放的需求,直接使用系統封裝的播放器太過於簡單,不能很好的滿足專案要求,於是花時間研究了一下,使用AVPlayer
來自定義播放器。
本視訊播放器主要自定義了帶緩衝顯示的進度條,可以拖動調節視訊播放進度的播放條,具有當前播放時間和總時間的Label,全屏播放功能,定時消失的工具條。播放器已經封裝到UIView
中,支援自動旋轉切換全屏,支援UITableView
。
主要功能
1.帶緩衝顯示的進度條
在自定義的時候,主要是需要計算當前進度和監聽緩衝的進度,細節方面需要注意進度顏色,進度為0的時候要設定為透明色,緩衝完成的時候需要設定顏色,不然全屏切換就會導致緩衝完成的進度條顏色消失。
- 自定義進度條的程式碼
#pragma mark - 建立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;
//進度條顏色
_progress.trackTintColor = ProgressColor;
// 計算緩衝進度
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)
{
//緩衝進度顏色
_progress.progressTintColor = ProgressTintColor;
}
else
{
//緩衝進度顏色
_progress.progressTintColor = [UIColor clearColor];
}
}
else
{
//緩衝進度顏色
_progress.progressTintColor = [UIColor clearColor];
}
[_bottomView addSubview:_progress];
}
複製程式碼
- 緩衝進度計算和監聽程式碼
#pragma mark - 快取條監聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"loadedTimeRanges"])
{
// 計算緩衝進度
NSTimeInterval timeInterval = [self availableDuration];
CMTime duration = _playerItem.duration;
CGFloat totalDuration = CMTimeGetSeconds(duration);
[_progress setProgress:timeInterval / totalDuration animated:NO];
//設定快取進度顏色
_progress.progressTintColor = ProgressTintColor;
}
}
複製程式碼
2.可以拖動調節視訊播放進度的播放條
這裡主要需要注意的是建立的播放條需要比進度條稍微長一點,這樣才可以看到滑塊從開始到最後走完整個進度條。播放條最好單獨新建一個繼承自UISlider
的控制元件,因為進度條和播放條的大小很可能不能完美的重合在一起,這樣看起來就會有2條線條,很不美觀,內部程式碼將其預設長度和起點重新佈局。
- 播放條控制元件內部程式碼
這裡重寫- (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);
}
複製程式碼
- 建立播放條程式碼
#pragma mark - 建立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];
//結束拖拽
[_slider addTarget:self
action:@selector(processSliderEndDragAction:)
forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
//左邊顏色
_slider.minimumTrackTintColor = PlayFinishColor;
//右邊顏色
_slider.maximumTrackTintColor = [UIColor clearColor];
}
複製程式碼
- 拖動播放條程式碼
#pragma mark - 拖動進度條
//開始
- (void)processSliderStartDragAction:(UISlider *)slider
{
//暫停
[self pausePlay];
[_timer invalidate];
}
//結束
- (void)processSliderEndDragAction:(UISlider *)slider
{
//繼續播放
[self playVideo];
_timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
target:self
selector:@selector(disappear)
userInfo:nil
repeats:NO];
}
//拖拽中
- (void)sliderValueChangedAction:(UISlider *)slider
{
//計算出拖動的當前秒數
CGFloat total = (CGFloat)_playerItem.duration.value / _playerItem.duration.timescale;
NSInteger dragedSeconds = floorf(total * slider.value);
//轉換成CMTime才能給player來控制播放進度
CMTime dragedCMTime = CMTimeMake(dragedSeconds, 1);
[_player seekToTime:dragedCMTime];
}
複製程式碼
3.具有當前播放時間和總時間的Label
建立時間顯示Label
的時候,我們需要建立一個定時器,每秒執行一下程式碼,來實現動態改變Label
上的時間顯示。
- Label建立程式碼
#pragma mark - 建立播放時間
- (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;
//當前進度
_slider.value = CMTimeGetSeconds([_playerItem currentTime]) / (_playerItem.duration.value / _playerItem.duration.timescale);
//當前時長進度progress
NSInteger proMin = (NSInteger)CMTimeGetSeconds([_player currentTime]) / 60;//當前秒
NSInteger proSec = (NSInteger)CMTimeGetSeconds([_player currentTime]) % 60;//當前分鐘
//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];
}
//開始播放停止轉子
if (_player.status == AVPlayerStatusReadyToPlay)
{
[_activity stopAnimating];
}
else
{
[_activity startAnimating];
}
}
複製程式碼
4.全屏播放功能
上面都是一些基本功能,最重要的還是全屏功能的實現。全屏功能這裡多說一下,由於我將播放器封裝到一個UIView
裡邊,導致在做全屏的時候出現了一些問題。因為播放器被封裝起來了,全屏的時候,播放器的大小就很可能超出父類控制元件的大小範圍,造成了超出部分點選事件無法獲取,最開始打算重寫父類-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法,但是想到這樣做就沒有達到封裝的目的,於是改變了一下思路,在全屏的時候,將播放器新增到Window
上,這樣播放器就不會超出父類的範圍大小,小屏的時候將播放器從Window
上還原到原有的父類上。
- 全屏程式碼 全屏的適配採用的是遍歷刪除原有控制元件,重新佈局建立全屏控制元件的方法實現。
#pragma mark - 全屏按鈕響應事件
- (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)];
//建立全屏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)];
//建立小屏UI
[self creatUI];
}
複製程式碼
- 建立播放器UI的程式碼
#pragma mark - 建立播放器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];
// 監聽loadedTimeRanges屬性
[_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
//建立播放按鈕
[self createButton];
//建立進度條
[self createProgress];
//建立播放條
[self createSlider];
//建立時間Label
[self createCurrentTimeLabel];
//建立返回按鈕
[self createBackButton];
//建立全屏按鈕
[self createMaxButton];
//建立點選手勢
[self createGesture];
//計時器,迴圈執行
[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.定時消失的工具條
如果工具條是顯示狀態,不點選視訊,預設一段時間後,自動隱藏工具條,點選視訊,直接隱藏工具條;如果工具條是隱藏狀態,點選視訊,就讓工具條顯示。功能說起來很簡單,最開始的時候,我使用GCD延遲程式碼實現,但是當點選讓工具條顯示,然後再次點選讓工具條消失,多點幾下你會發現你的定時消失時間不對。這裡我們需要注意的是,當你再次點選的時候需要取消上一次的延遲執行程式碼,才能夠讓下一次點選的時候,延遲程式碼正確執行。這裡採用定時器來實現,因為定時器可以取消延遲執行的程式碼。
- 點選視訊的程式碼
#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;
/**旋轉自動全屏,預設Yes*/
@property (nonatomic,assign) BOOL autoFullScreen;
/**重複播放,預設No*/
@property (nonatomic,assign) BOOL repeatPlay;
/**是否支援橫屏,預設No*/
@property (nonatomic,assign) BOOL isLandscape;
/**播放*/
- (void)playVideo;
/**暫停*/
- (void)pausePlay;
/**返回按鈕回撥方法*/
- (void)backButton:(BackButtonBlock) backButton;
/**播放完成回撥*/
- (void)endPlay:(EndBolck) end;
/**銷燬播放器*/
- (void)destroyPlayer;
/**
根據播放器所在位置計算是否滑出螢幕,
@param tableView Cell所在tableView
@param cell 播放器所在Cell
@param beyond 滑出後的回撥
*/
- (void)calculateWith:(UITableView *)tableView cell:(UITableViewCell *)cell beyond:(BeyondBlock) beyond;
複製程式碼
- 使用方法
直接使用cocoapods匯入,pod 'CLPlayer'
- 具體使用程式碼
CLPlayerView *playerView = [[CLPlayerView alloc] initWithFrame:CGRectMake(0, 90, ScreenWidth, 300)];
[self.view addSubview:playerView];
//根據旋轉自動支援全屏,預設支援
// playerView.autoFullScreen = NO;
//重複播放,預設不播放
// playerView.repeatPlay = YES;
//如果播放器所在頁面支援橫屏,需要設定為Yes,不支援不需要設定(預設不支援)
// playerView.isLandscape = YES;
//視訊地址
playerView.url = [NSURL URLWithString:@"http://wvideo.spriteapp.cn/video/2016/0215/56c1809735217_wpd.mp4"];
//播放
[playerView playVideo];
//返回按鈕點選事件回撥
[playerView backButton:^(UIButton *button) {
NSLog(@"返回按鈕被點選");
}];
//播放完成回撥
[playerView endPlay:^{
//銷燬播放器
[playerView destroyPlayer];
playerView = nil;
NSLog(@"播放完成");
}];
複製程式碼
說明
UIImage+TintColor
是用來渲染圖片顏色的分類,由於缺少圖片資源,所以採用其他顏色圖片渲染成自己需要的顏色;UIImage+ScaleToSize
這個分類是用來改變圖片尺寸大小的,因為播放條中的滑塊不能直接改變大小,所以通過改變圖片尺寸大小來控制滑塊大小;UIView+SetRect
是用於適配的分類。
總結
在自定義播放器的時候,需要注意的細節太多,這裡就不一一細說了,更多細節請看Demo,Demo中有很詳細的註釋。考慮到大部分APP不支援橫屏,播放器預設是不支援橫屏的,如果需要支援橫屏(勾選了支援左右方向),建立播放器的時候,寫上這句程式碼playerView.isLandscape = YES;
。
播放器效果圖
Demo地址
最近更新修改了很多地方的程式碼,主要是使用Masonry來重構了一下工具條,修復了一些bug,具體還請參考CLPlayer 如果喜歡,歡迎star。