最近在學習AVFoundation框架的相關知識,寫了一個基於AVPlayer的視頻播放器,相關功能如下圖:
代碼github:https://github.com/wzpziyi1/VideoPlayer
AVFoundation都是圍繞AVPlayer展開的,AVPlayer是一個用來播放基於時間的視聽媒體的控制器對象。但它與我們通常理解的"控制器"不同,它不是一個視圖或者窗口控制器,而是一個對播放和資源時間相關信息進行管理的對象,具體的用戶界面是需要開發者自己進行編寫的。
AVPlayer是一個不可見的組件,如果需要有可視化的界面,那么需要使用AVPlayerLayer類。AVPlayerLayer構建於Core Animation之上,它擴展了Core Animation的CALayer類,並通過框架在屏幕上顯示視頻內容(它只是用作視頻內容的渲染面,其他可視化界面需要自己編寫)。創建AVPlayerLayer需要一個指向AVPlayer實例的指針,這就可以將圖層和播放器綁定在一起了,保證了當播放器基於時間的方法出現時使二者保持同步。
AVPlayerItem
它會建立媒體資源動態視角的數據模型,並保存AVPlayer在播放時的呈現狀態。
例如seekToTime:方法:
__weak typeof(self) tmp = self;
[self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
if (finished)
{
tmp.transport.isFinishedJump = YES;
}
}];
//跳轉到哪個時間點進行播放
AVPlayerItem有兩個很重要的屬性,一個是status,一個是loadedTimeRanges。使用kvo監聽status屬性值的改變,當status變為
AVPlayerStatusReadyToPlay的時候,表示可以開始播放了。而監聽loadedTimeRanges屬性,可以知道視頻緩存到哪樣一個時間點了:
//kvo
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
AVPlayerItem *item = (AVPlayerItem *)object;
if ([keyPath isEqualToString:@"status"])
{
NSLog(@"%d", (int)[item status]);
if ([item status] == AVPlayerStatusReadyToPlay)
{
[self monitorPlayingStatusWithItem:item];
}
}
else if ([keyPath isEqualToString:@"loadedTimeRanges"]) //緩沖
{
self.bufferTime = [self availableBufferTime];
if (!self.isFetchTotalDuration)
{
//獲取視頻總長度
NSTimeInterval totalDuration = CMTimeGetSeconds(item.duration);
self.transport.durationTime = totalDuration;
self.isFetchTotalDuration = YES;
}
if (self.transport.currentPlayTime <= self.transport.durationTime - 12)
{
//如果緩沖不夠
if (self.bufferTime <= self.transport.currentPlayTime + 10)
{
self.transport.isBuffering = YES;
}
else
{
self.transport.isBuffering = NO;
}
}
else
{
self.transport.isBuffering = NO;
}
self.transport.currentBufferTime = self.bufferTime;
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
在這段代碼里面,我是設置了,如果當前播放時間+10s > 緩沖得到的總時間,那么就loading,直到緩沖時間總是大於當前播放時間10s,才開始播放。
CMTime
AVFoundation使用基於CMTime的數據結構來展示時間信息,它表現為分數值的方式,具體定義如下:
/*!
@typedef CMTime
@abstract Rational time value represented as int64/int32.
*/
typedef struct
{
CMTimeValue value; /*! @field value The value of the CMTime. value/timescale = seconds. */
CMTimeScale timescale; /*! @field timescale The timescale of the CMTime. value/timescale = seconds. */
CMTimeFlags flags; /*! @field flags The flags, eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity, etc. */
CMTimeEpoch epoch; /*! @field epoch Differentiates between equal timestamps that are actually different because
of looping, multi-item sequencing, etc.
Will be used during comparison: greater epochs happen after lesser ones.
Additions/subtraction is only possible within a single epoch,
however, since epoch length may be unknown/variable. */
} CMTime;
在這個結構中,主要關注value和timescale,它們在時間呈現中分別作為分子和分母。
CMTimeMake(1, 2); //0.5s
CMTimeMake(4, 1); //4s
CMTimeMakeWithSeconds(60, NSEC_PER_SEC); //表示將60秒轉化為CMTime時間格式
在這個demo中,可視化界面的編寫主要在ZYOverlayView.h類里面,它負責用戶的絕大部分交互事件。ZYOverlayView遵守ZYTransport協議,這個協議提供了與ViewController里面的AVPlayer進行通信的接口,相應代碼如下:
#import <UIKit/UIKit.h> #import <AVFoundation/AVFoundation.h> @protocol ZYTransportDelegate <NSObject> - (void)play; - (void)pause; - (void)stop; /** * 跳轉到某個時間點播放 * */ - (void)jumpedToTime:(NSTimeInterval)time; /** * 視頻橫屏 * * @param flag YES為是 */ - (void)fullScreenOrNormalSizeWithFlag:(BOOL)flag; @end @protocol ZYTransport <NSObject> /** * 是否正在緩沖 */ @property (nonatomic, assign) BOOL isBuffering; /** * 是否完成跳轉 */ @property (nonatomic, assign) BOOL isFinishedJump; @property (nonatomic, assign) NSTimeInterval durationTime; @property (nonatomic, weak) id<ZYTransportDelegate>delegate; /** * 設置當前播放的時間點 */ @property (nonatomic, assign) NSTimeInterval currentPlayTime; /** * 設置當前緩沖的時間點 */ @property (nonatomic, assign) NSTimeInterval currentBufferTime; /** * 視頻播放完畢 */ - (void)playbackComplete; @end
如果要視頻可以顯示,那么一定需要AVPlayeLayer來做視頻內容的渲染面,上面提到了可視化界面的編寫是在ZYPlayerView類里面實現的,所在我在這個類里面加載ZYOverlayView,以便實現相關功能的封裝。
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "ZYTransport.h"
@interface ZYPlayerView : UIView
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, weak) id<ZYTransport>transport;
@end
#import "ZYPlayerView.h"
#import "ZYOverlayView.h"
@interface ZYPlayerView ()
@property (nonatomic, strong) ZYOverlayView *overlayView;
@end
@implementation ZYPlayerView
+ (Class)layerClass
{
return [AVPlayerLayer class];
}
- (instancetype)init
{
if (self = [super init])
{
[self commitInit];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self commitInit];
}
- (void)commitInit
{
self.overlayView = [ZYOverlayView overlayView];
[self addSubview:self.overlayView];
[self.overlayView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
}
- (AVPlayer *)player
{
return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player
{
[(AVPlayerLayer *)[self layer] setPlayer:player];
}
- (id<ZYTransport>)transport
{
return self.overlayView;
}
@end
ViewController主要是在做監聽播放狀態、緩沖狀態,與可視化界面之間的通信,將監聽到的當前播放時間、當前緩沖時間、播放結束等傳遞給遵守了ZYTransport協議的ZYOverlayView實例,在OverlayView實例里面進行相關操作。與可視化界面之間的通信主要表現為,暫停、播放、停止、全屏等,這些在ZYTransport協議里面都有相關的代理方法。
#import "ZYPlayerVc.h"
#import "ZYPlayerView.h"
#import <AVFoundation/AVFoundation.h>
#import "ZYTransport.h"
@interface ZYPlayerVc () <ZYTransportDelegate>
@property (strong, nonatomic) ZYPlayerView *playerView;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerItem *playerItem;
@property (nonatomic, weak) id<ZYTransport>transport;
@property (nonatomic, assign) CGFloat scale;
/**
* 是否獲取了視頻長度
*/
@property (nonatomic, assign) BOOL isFetchTotalDuration;
@property (nonatomic, strong) id timeObserver;
@property (nonatomic, assign) NSTimeInterval bufferTime;
@end
@implementation ZYPlayerVc
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.isFetchTotalDuration = NO;
NSURL *url = [NSURL URLWithString:@"http://v.jxvdy.com/sendfile/w5bgP3A8JgiQQo5l0hvoNGE2H16WbN09X-ONHPq3P3C1BISgf7C-qVs6_c8oaw3zKScO78I--b0BGFBRxlpw13sf2e54QA"];
self.playerItem = [[AVPlayerItem alloc] initWithURL:url];
//監聽status屬性
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];
self.playerView.player = self.player;
self.playerView.frame = CGRectMake(0, 30, kScreenW, kScreenW * kScreenW / kScreenH);
self.scale = kScreenW / self.playerView.height;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[UIApplication sharedApplication] setStatusBarHidden:YES];
}
- (ZYPlayerView *)playerView
{
if (_playerView == nil)
{
_playerView = [[ZYPlayerView alloc] init];
_playerView.backgroundColor = [UIColor blackColor];
self.transport = _playerView.transport;
self.transport.isBuffering = YES;
self.transport.delegate = self;
[self.view addSubview:_playerView];
}
return _playerView;
}
- (void)moviePlayDidEnd:(NSNotification *)note
{
self.isFetchTotalDuration = NO;
__weak typeof(self) tmp = self;
[self.player seekToTime:kCMTimeZero completionHandler:^(BOOL finished) {
tmp.transport.currentPlayTime = 0;
tmp.transport.currentBufferTime = 0;
tmp.transport.durationTime = 0;
}];
}
//kvo
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
AVPlayerItem *item = (AVPlayerItem *)object;
if ([keyPath isEqualToString:@"status"])
{
NSLog(@"%d", (int)[item status]);
if ([item status] == AVPlayerStatusReadyToPlay)
{
[self monitorPlayingStatusWithItem:item];
}
}
else if ([keyPath isEqualToString:@"loadedTimeRanges"]) //緩沖
{
self.bufferTime = [self availableBufferTime];
if (!self.isFetchTotalDuration)
{
//獲取視頻總長度
NSTimeInterval totalDuration = CMTimeGetSeconds(item.duration);
self.transport.durationTime = totalDuration;
self.isFetchTotalDuration = YES;
}
if (self.transport.currentPlayTime <= self.transport.durationTime - 7)
{
//如果緩沖不夠
if (self.bufferTime <= self.transport.currentPlayTime + 5)
{
self.transport.isBuffering = YES;
}
else
{
self.transport.isBuffering = NO;
}
}
else
{
self.transport.isBuffering = NO;
}
self.transport.currentBufferTime = self.bufferTime;
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
/**
* 監聽播放狀態
*
*/
- (void)monitorPlayingStatusWithItem:(AVPlayerItem *)item
{
__weak typeof(self) tmp = self;
self.timeObserver = [self.playerView.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
NSTimeInterval currentTime = CMTimeGetSeconds(time);
tmp.transport.currentPlayTime = currentTime;
}];
}
/**
* 可用的播放時長(緩沖的時長)
*
*/
- (NSTimeInterval)availableBufferTime
{
NSArray *loadTimeRanges = [[self.player currentItem] loadedTimeRanges];
//獲取緩沖區域
CMTimeRange range = [loadTimeRanges.firstObject CMTimeRangeValue];
NSTimeInterval startTime = CMTimeGetSeconds(range.start);
NSTimeInterval duration = CMTimeGetSeconds(range.duration);
return startTime + duration;
}
#pragma mark ----ZYTransportDelegate
- (void)play
{
[self.playerView.player play];
}
- (void)pause
{
[self.playerView.player pause];
}
- (void)stop
{
self.isFetchTotalDuration = NO;
[self moviePlayDidEnd:nil];
}
/**
* 跳轉到某個時間點播放
*
*/
- (void)jumpedToTime:(NSTimeInterval)time
{
__weak typeof(self) tmp = self;
[self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
if (finished)
{
tmp.transport.isFinishedJump = YES;
}
}];
}
/**
* 視頻橫豎屏
*
* @param flag YES為是
*/
- (void)fullScreenOrNormalSizeWithFlag:(BOOL)flag
{
if (flag)
{
CGFloat moveY = self.view.center.y - self.playerView.center.y;
CGAffineTransform tmpTransform = CGAffineTransformScale(CGAffineTransformMakeTranslation(0, moveY), self.scale, self.scale);
CGAffineTransform transform = CGAffineTransformRotate(tmpTransform, - M_PI_2);
[UIView animateWithDuration:0.25 animations:^{
self.playerView.transform = transform;
}];
}
else
{
[UIView animateWithDuration:0.25 animations:^{
self.playerView.transform = CGAffineTransformIdentity;
}];
}
}
- (void)dealloc
{
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
[self.playerView.player removeTimeObserver:self.timeObserver];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
ZYOverlayView里面最主要處理的是一個拖動滑塊的事件和5秒后如果沒有接到觸碰事件隱藏Tool。拖動滑塊的話,我是弄了一個pan手勢,然后在里面計算相關位置的問題。這里有一個巨坑,一開始我並沒有給滑塊設置layout,只是想通過改變滑塊的center來做到位置隨着拖動長度的改變而改變,卻發現它在上下兩個Tool View隱藏的時候,回到了它的初始位置,后來給它加上layout,這個bug消失,暫時沒找到bug產生原因。
基於這樣一個bug,我本來是想把所有的布局改成frame布局的,但是改完之后發現,底部的Tool View根本不顯示,又是一個未知原因的bug。找了很久,沒明白導致這個bug產生的原因......所有我就都改成layout布局,並且未了防止總是刷新layout,在改變const的同時,將它們的frame也相應的改變了。
沒有接到觸碰事件隱藏Tool,是基於定時器實現的。每隔五秒,如果在五秒內有接受到任意觸碰事件,就讓定時器先invalidate,在resetTimer,這樣確保定時器時間可以實時處於更新狀態。相應代碼:
#import <UIKit/UIKit.h>
#import "ZYTransport.h"
@interface ZYOverlayView : UIView <ZYTransport>
@property (nonatomic, assign) NSTimeInterval durationTime;
@property (nonatomic, weak) id<ZYTransportDelegate>delegate;
/**
* 是否正在緩沖
*/
@property (nonatomic, assign) BOOL isBuffering;
@property (nonatomic, assign) BOOL isFinishedJump;
@property (nonatomic, assign) NSTimeInterval currentPlayTime;
@property (nonatomic, assign) NSTimeInterval currentBufferTime;
+ (instancetype)overlayView;
@end
#import "ZYOverlayView.h"
@interface ZYOverlayView ()
/**
* 總進度
*/
@property (weak, nonatomic) IBOutlet UIView *totalProgressView;
@property (weak, nonatomic) IBOutlet UIView *bufferProgressView;
/**
* 進度上面的顯示時間
*/
@property (weak, nonatomic) IBOutlet UIButton *progressTimeBtn;
/**
* 當前播放時間VIew
*/
@property (weak, nonatomic) IBOutlet UILabel *currentTimeLabel;
/**
* 總進度View
*/
@property (weak, nonatomic) IBOutlet UILabel *totalTimeLabel;
/**
* 滑塊
*/
@property (weak, nonatomic) IBOutlet UIImageView *sliderView;
/**
* 當前緩沖進度的寬度
*/
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *currentProgressConW;
@property (weak, nonatomic) IBOutlet UIView *topView;
@property (weak, nonatomic) IBOutlet UIView *bottomView;
@property (weak, nonatomic) IBOutlet UIButton *playOrPauseBtn;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *indicatorView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *topViewConTop;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomViewConBottom;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *sliderConLeft;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *progressTimeConLeft;
/**
* 是否為橫屏
*/
@property (nonatomic, assign) BOOL isfullScreen;
/**
* 控制top\bottom View的隱藏定時器
*/
@property (nonatomic, strong) NSTimer *timer;
/**
* 判斷是否需要控制top/bottom View隱藏
*/
@property (nonatomic, assign) BOOL isControlHidden;
/**
* top\bottom View是否正在展示
*/
@property (nonatomic, assign) BOOL isShowing;
/**
* 不是由於緩沖或者拖動滑塊導致的暫停(也就是必然的暫停,交互時的暫停)
*/
@property (nonatomic, assign) BOOL isCertainPause;
/**
* 是否正在拖動滑塊
*/
@property (nonatomic, assign) BOOL isDraggingSlider;
@end
@implementation ZYOverlayView
+ (instancetype)overlayView
{
return [[self alloc] init];
}
- (instancetype)init
{
if (self = [super init])
{
self = [[[NSBundle mainBundle] loadNibNamed:@"ZYOverlayView" owner:nil options:nil] lastObject];
[self commitInit];
}
return self;
}
- (void)commitInit
{
self.isfullScreen = NO;
self.isControlHidden = YES;
self.isShowing = YES;
self.indicatorView.hidden = YES;
self.isCertainPause = NO;
self.isDraggingSlider = NO;
self.progressTimeBtn.hidden = YES;
[self.indicatorView startAnimating];
self.layer.masksToBounds = YES;
self.sliderView.userInteractionEnabled = YES;
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(draggingSlider:)];
[self.sliderView addGestureRecognizer:panRecognizer];
[self resetTimer];
}
- (void)setDurationTime:(NSTimeInterval)durationTime
{
_durationTime = durationTime;
self.totalTimeLabel.text = [self converTimeToStringWithTime:durationTime];
}
- (void)setIsBuffering:(BOOL)isBuffering
{
_isBuffering = isBuffering;
if (self.isCertainPause) return;
self.isCertainPause = NO;
//由btn的tag來判斷此次事件是交互的暫停,還是緩沖導致的暫停
self.playOrPauseBtn.tag = 1;
if (isBuffering)
{
self.indicatorView.hidden = NO;
self.playOrPauseBtn.enabled = NO;
if (self.playOrPauseBtn.selected)
{
[self clickPlayOrPauseBtn:self.playOrPauseBtn];
}
}
else
{
self.indicatorView.hidden = YES;
self.playOrPauseBtn.enabled = YES;
if (!self.playOrPauseBtn.selected)
{
[self clickPlayOrPauseBtn:self.playOrPauseBtn];
}
}
self.playOrPauseBtn.tag = 0;
}
- (void)setCurrentPlayTime:(NSTimeInterval)currentPlayTime
{
_currentPlayTime = currentPlayTime;
if (!self.isDraggingSlider)
{
self.currentTimeLabel.text = [self converTimeToStringWithTime:currentPlayTime];
self.sliderView.centerX = (currentPlayTime / _durationTime) * self.totalProgressView.width;
_sliderConLeft.constant = self.totalProgressView.x + self.sliderView.centerX - self.sliderView.width / 2;
}
}
- (void)setCurrentBufferTime:(NSTimeInterval)currentBufferTime
{
_currentBufferTime = currentBufferTime;
self.bufferProgressView.width = self.totalProgressView.width * currentBufferTime / _durationTime;
self.currentProgressConW.constant = self.totalProgressView.width * currentBufferTime / _durationTime;
}
- (void)setIsFinishedJump:(BOOL)isFinishedJump
{
_isFinishedJump = isFinishedJump;
if (isFinishedJump)
{
self.isDraggingSlider = NO;
}
_isFinishedJump = NO;
}
- (IBAction)clickFinishBtn:(id)sender
{
[self.timer invalidate];
if ([self.delegate respondsToSelector:@selector(stop)])
{
[self.delegate stop];
}
[self resetTimer];
}
- (IBAction)clickFillScreenBtn:(id)sender
{
[self.timer invalidate];
self.isfullScreen = !self.isfullScreen;
if ([self.delegate respondsToSelector:@selector(fullScreenOrNormalSizeWithFlag:)])
{
[self.delegate fullScreenOrNormalSizeWithFlag:self.isfullScreen];
}
[self resetTimer];
}
- (IBAction)clickPlayOrPauseBtn:(UIButton *)sender
{
[self.timer invalidate];
[self resetTimer];
if (!sender.tag && sender.selected)
{
self.isCertainPause = YES;
}
else
{
self.isCertainPause = NO;
}
sender.selected = !sender.selected;
if (sender.selected)
{
if ([self.delegate respondsToSelector:@selector(play)])
{
[self.delegate play];
}
}
else
{
if ([self.delegate respondsToSelector:@selector(pause)])
{
[self.delegate pause];
}
}
}
/**
* 拖動滑塊的時候
*
*/
- (void)draggingSlider:(UIPanGestureRecognizer *)recognizer
{
[self.timer invalidate];
CGPoint point = [recognizer translationInView:self.bottomView];
[recognizer setTranslation:CGPointZero inView:self.bottomView];
CGFloat x = point.x;
self.isDraggingSlider = YES;
if (recognizer.state == UIGestureRecognizerStateEnded)
{
[self resetTimer];
self.progressTimeBtn.hidden = YES;
CGFloat jumpedTime = self.durationTime * (self.sliderView.centerX - self.totalProgressView.x) / self.totalProgressView.width;
if ([self.delegate respondsToSelector:@selector(jumpedToTime:)])
{
[self.delegate jumpedToTime:jumpedTime];
}
}
else
{
self.progressTimeBtn.hidden = NO;
self.sliderView.centerX += x;
CGPoint point = [self.bottomView convertPoint:self.sliderView.center toView:self];
self.progressTimeBtn.centerY = point.y - 40;
self.progressTimeBtn.centerX = point.x;
if (self.sliderView.centerX > self.totalProgressView.x + self.totalProgressView.width)
{
self.sliderView.centerX = self.totalProgressView.x + self.totalProgressView.width;
}
if (self.sliderView.centerX < self.totalProgressView.x)
{
self.sliderView.centerX = self.totalProgressView.x;
}
//取消一切動畫效果(一般來說,用來禁止隱式動畫)
[UIView setAnimationsEnabled:NO];
CGFloat jumpedTime = self.durationTime * (self.sliderView.centerX - self.totalProgressView.x) / self.totalProgressView.width;
NSString *timeStr = [self converTimeToStringWithTime:jumpedTime];
self.progressTimeBtn.titleLabel.text = timeStr;
[self.progressTimeBtn setTitle:timeStr forState:UIControlStateNormal];
[UIView setAnimationsEnabled:YES];
self.progressTimeConLeft.constant = self.sliderView.centerX - self.progressTimeBtn.width / 2;
_sliderConLeft.constant = self.sliderView.centerX - self.sliderView.width / 2;
}
}
#pragma mark ----NSTimer相關操作
- (void)resetTimer
{
[self.timer invalidate];
self.timer = nil;
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(updateTimer) userInfo:nil repeats:NO];
}
- (void)updateTimer
{
if (!self.timer.isValid || !self.timer || !self.isControlHidden) return;
[self hideTopAndBottomView];
}
#pragma mark ----控制top\bottom 隱藏or顯示相關邏輯
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self showTopAndBottomView];
}
- (void)showTopAndBottomView
{
[self.timer invalidate];
if (!self.isShowing)
{
self.topView.hidden = NO;
self.bottomView.hidden = NO;
self.topViewConTop.constant = 0;
self.bottomViewConBottom.constant = 0;
[UIView animateWithDuration:0.3 animations:^{
[self layoutIfNeeded];
}completion:^(BOOL finished) {
self.isShowing = YES;
self.isControlHidden = YES;
}];
}
[self resetTimer];
}
- (void)hideTopAndBottomView
{
[self.timer invalidate];
if (self.isShowing)
{
self.topViewConTop.constant = -50;
self.bottomViewConBottom.constant = -50;
[UIView animateWithDuration:0.3 animations:^{
[self layoutIfNeeded];
} completion:^(BOOL finished) {
self.topView.hidden = YES;
self.bottomView.hidden = YES;
self.isShowing = NO;
self.isControlHidden = NO;
}];
}
}
#pragma mark ----other
- (NSString *)converTimeToStringWithTime:(NSTimeInterval)time
{
int hour = time / 60 / 60;
int minute = (time - hour * 60 * 60) / 60;
int second = (int)time % 60;
return [NSString stringWithFormat:@"%02d:%02d:%02d", hour, minute, second];
}
- (void)layoutSubviews
{
[super layoutSubviews];
}
@end
代碼github地址:https://github.com/wzpziyi1/VideoPlayer


