ios-自定义AVPlayer-可以点击拖动的进度条
上图:
上代码:
#import "FQQVideoPlayerViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "BottomView.h"
typedef NS_ENUM(NSInteger, PlayerStatu){
None,
End,
Play,
Pause
};
@interface FQQVideoPlayerViewController () <BottomViewDelegate>
@property (nonatomic) CGFloat duration;
@property (nonatomic) CGFloat fps;
@property (nonatomic) AVPlayer *player;
@property (nonatomic) id playerObserve;
@property (nonatomic) BottomView *bottomView;
@property (nonatomic) PlayerStatu playerStatu;
@property (nonatomic) BOOL isSliding;
@property (nonatomic) UITapGestureRecognizer *tap;
@end
@implementation FQQVideoPlayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//如果不是ios10.1
if(![self isIOS10_1]){
[self initAVPlayer];
}
[self initBottomView];
}
- (void)viewDidAppear:(BOOL)animated{
if([self isIOS10_1]){
[self initAVPlayer];
[self.view bringSubviewToFront:_bottomView];
}
[_player play];
_playerStatu = Play;
}
说明一下,AVplayer在ios10.1有个BUG,在viewDidLoad初始化有很大几率发生AVPlayerItemStatusFailed,然后avplayer就是黑乎乎的。在其他系统一切正常。不过你把初始化放在viewDidApperar,一切也正常。这个解决方法开始是因为一个同事说他在viewDidLoad用dispatch_after等个5秒后再初始化能解决这个问题,然后我的直觉告诉我会不会在viewWillAppear或viewDidAppear就可以了呢。一试,果然,viewDidAppear完美运行。别问我为什么,就是直觉。
- (void)initAVPlayer{
AVPlayerItem *item = [[AVPlayerItem alloc]initWithURL:_url];
_duration = CMTimeGetSeconds(item.asset.duration);
_fps = [[[item.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] nominalFrameRate];
_player = [[AVPlayer alloc]initWithPlayerItem:item];
AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:_player];
layer.frame = self.view.bounds;
layer.videoGravity = AVLayerVideoGravityResizeAspect;
[self.view.layer addSublayer:layer];
//这里监听avplayer是否播放完成,完成调用playDidFinished:
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(playDidFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:item];
//用弱引用避免互相引用
__weak typeof(self) weakSelf = self;
//对于1分钟以内的视频就每1/30秒刷新一次页面,大于1分钟的每秒一次就行
CMTime interval = _duration > 60 ? CMTimeMake(1, 1) : CMTimeMake(1, 30);
//这个方法就是每隔多久调用一次block,函数返回的id类型的对象在不使用时用-removeTimeObserver:释放,官方api是这样说的
_playerObserve = [_player addPeriodicTimeObserverForInterval:interval queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
if(!weakSelf.isSliding){
CGFloat currentTime = CMTimeGetSeconds(time);
NSString *timeText = [NSString stringWithFormat:@"%@/%@", [weakSelf convert:currentTime], [weakSelf convert:weakSelf.duration]];
weakSelf.bottomView.timeLabel.text = timeText;
weakSelf.bottomView.slider.value = currentTime / weakSelf.duration;
}
}];
}
- (void)playDidFinished:(NSNotification *)notification{
[_bottomView.playButton setBackgroundImage:[UIImage imageNamed:@"btn_video_play"] forState:UIControlStateNormal];
[_player pause];
_playerStatu = End;
}
关于duration,AVPlayerItem里也有一个duration,不过你直接拿_duration = CMTimeGetSeconds(item.duration),打印出来是NAN(南!?),_duration = CMTimeGetSeconds(item.asset.duration)是正确的。好奇怪,难道AVPlayerItem的duration不是从item里的asset里取的吗?
接下来是BottomView
#import <UIKit/UIKit.h>
@protocol BottomViewDelegate
- (void)play;
@end
@interface BottomView : UIView
@property (nonatomic, weak) id<BottomViewDelegate> delegate;
@property (weak, nonatomic) IBOutlet UIButton *playButton;
@property (weak, nonatomic) IBOutlet UILabel *timeLabel;
@property (weak, nonatomic) IBOutlet UISlider *slider;
@end
@implementation BottomView
- (void)awakeFromNib{
[super awakeFromNib];
[_slider setThumbImage:[UIImage imageNamed:@"tab_progress_dot"] forState:UIControlStateNormal];
}
- (IBAction)play:(id)sender {
[_delegate play];
}
@end
没错,进度条就是用UISlider来实现的。但是你一定要拖动那个圆点才能拖动,点击是徒劳的。为什么不把点击效果也弄上呢,苹果的官方控件有时就是这么莫名其妙啊,TimePicker的文字颜色大小不能改,TextField内部明明有placeHolder但又不给使用,页面布局竟然没有TableLayout,LinearLayout?(楼主是Android转过来的)只好自己加上点击事件
- (void)initBottomView{
_bottomView = [[[NSBundle mainBundle]loadNibNamed:@"BottomView" owner:self options:nil]firstObject];
_bottomView.frame = CGRectMake(0, 667 - 125, 375, 125);
_bottomView.delegate = self;
[self.view addSubview:_bottomView];
[_bottomView.slider addTarget:self action:@selector(handleTouchDown:) forControlEvents:UIControlEventTouchDown];
[_bottomView.slider addTarget:self action:@selector(handleSlide:) forControlEvents:UIControlEventValueChanged];
[_bottomView.slider addTarget:self action:@selector(handleTouchUp:) forControlEvents:UIControlEventTouchUpInside];
[_bottomView.slider addTarget:self action:@selector(handleTouchUp:) forControlEvents:UIControlEventTouchUpOutside];
_tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleTap:)];
[_tap setNumberOfTouchesRequired:1];
[_bottomView.slider addGestureRecognizer:_tap];
}
这样在大多数情况下运行是正常的,但如果你只是稍微的拖动slider,tap事件也会触发,详情可以看这里:http://blog.csdn.net/icetime17/article/details/50720545
- (void)handleTouchDown:(UISlider *)slider{
NSLog(@"TouchDown");
_tap.enabled = NO;
_isSliding = YES;
if(_playerStatu == Play){
[_player pause];
}
}
- (void)handleTouchUp:(UISlider *)slider{
NSLog(@"TouchUp");
_tap.enabled = YES;
_isSliding = NO;
if(_playerStatu == Play){
[_player play];
}
}
- (void)handleSlide:(UISlider *)slider{
CMTime time = CMTimeMakeWithSeconds(_duration * slider.value, _fps);
NSString *timeText = [NSString stringWithFormat:@"%@/%@", [self convert:_duration * slider.value], [self convert:_duration]];
_bottomView.timeLabel.text = timeText;
[_player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer{
NSLog(@"Tap");
CGPoint touchPoint = [recognizer locationInView:_bottomView.slider];
CGFloat value = touchPoint.x / CGRectGetWidth(_bottomView.slider.frame);
[_bottomView.slider setValue:value animated:YES];
if(_playerStatu == Play){
[_player pause];
}
CMTime time = CMTimeMakeWithSeconds(_duration * value, _fps);
[_player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
if(_playerStatu == Play){
[_player play];
}
}];
}
还要实现BottomView的delegate
- (void)play{
if(_playerStatu == End){
[_player seekToTime:kCMTimeZero toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
[_bottomView.playButton setBackgroundImage:[UIImage imageNamed:@"btn_video_stop"] forState:UIControlStateNormal];
[_player play];
_playerStatu = Play;
}];
}else if(_playerStatu == Play){
[_bottomView.playButton setBackgroundImage:[UIImage imageNamed:@"btn_video_play"] forState:UIControlStateNormal];
[_player pause];
_playerStatu = Pause;
}else if(_playerStatu == Pause){
[_bottomView.playButton setBackgroundImage:[UIImage imageNamed:@"btn_video_stop"] forState:UIControlStateNormal];
[_player play];
_playerStatu = Play;
}
}
再献上时间转化函数
- (NSString *)convert:(CGFloat)time{
int minute = time / 60;
int second = time - minute * 60;
NSString *minuteString;
NSString *secondString;
if(minute < 10){
minuteString = [NSString stringWithFormat:@"0%d", minute];
}else{
minuteString = [NSString stringWithFormat:@"%d", minute];
}
if(second < 10){
secondString = [NSString stringWithFormat:@"0%d", second];
}else{
secondString = [NSString stringWithFormat:@"%d", second];
}
return [NSString stringWithFormat:@"%@:%@", minuteString, secondString];
}
最后别忘记了
-(void)dealloc{
[_player removeTimeObserver:_playerObserve];
}
至于ios10.1的系统判断嘛
- (BOOL)isIOS10_1{
//TODO
return NO;
}
项目源码和Demo在Github上有,有兴趣的可以去看看
https://github.com/FQQA/iOS