自定义视频播放器与慢放滚轮
受同学之邀,帮忙自定义一控件。需求是:开发慢放滚轮,用手指拨动实现帧级的慢速播放,滚轮可双向拨动,其滚动具有惯性,滚动速度决定视频播放的速度。需求很明朗,可我却是一头雾水。说实话,在此之前我还没有自定义过视频播放器,不懂怎么实现‘帧级慢速播放’。并且滚轮这个东西自定义起来,我也是没谱的。越是自己觉得陌生的东西,越是想办法回避,想着挑战一下自己,就尝试着做了起来。
要实现这个需求,首先滚轮是要自定义的。再说视频播放器,如果要用别人写好的视频播放器框架,需要coder刚好提供了一个方法,专门控制视频播放进度的。为了避免繁琐,我基于系统的AVPlayer自定义了视频播放器,实现了相关功能。DCVideoPlyer


一、自定义慢放滚轮
设计思路:
1.滚轮有三个状态:开始触碰、持续滚动、结束触碰。基于这三个状态考虑,决定在Control上自定义。
2.滚轮看起来得有立体的感觉,刻度线间距应该有变化,并且长度也应该有变化,达到近大远小的效果。
3.滚轮上的刻度线是绕着圆心转动的,想象着平面上是一个半圆,刻度线的长度与间距从小到大,又从大到小变化,你是不是想起了高中所学三角函数,这里就是利用三角函数与反三角函数实现相关功能。
4.暂无

实现功能:

实现滚轮刻度线的整体效果,其关键核心就是利用三角函数,计算出每条线条的起始点。要验证函数的正确性,可将0、π/2、π,三个弧度代入检验。
/*!
@method 每条线初始点的集合。
@abstract 初始化每条的初始位置点,并记录下来。
@discussion 利用三角函数,反三角函数,将滚轮滑动的距离转换为弧度,并计算出每条线的初始点。
*/
- (void)resetLinesArray
{
[_linesArray removeAllObjects];
double arcTheLine = asin((_kRadius - _scroledRange)/ (_kRadius));
for (int i = 0; i < (LineCounts +1); i++) {
double temArc =arcTheLine - i *_intervalTwoLine ;
if (temArc < 0) {
temArc += M_PI;
}
CGPoint pt;
// 加π的原因是 滚轮转动的方向跟滑动方向相反,故加上π调整过来。
pt.x = _kRadius - _kRadius*cos(temArc+M_PI);
pt.y = (_layerLength * 0.5 + _layerLength* 0.5*sin(temArc+M_PI));
[_linesArray addObject:[NSValue valueWithCGPoint:pt]];
}
}
画出刻度线:
/*!
@method 画出每条刻度线。
@abstract 利用CGContext 画出每条刻度线。
@discussion 根据三角函数算出的点,转换成长短不一,距离不一的刻度线。
@param layer layer 图层
@param ctx 上下文
*/
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if (layer == self.layer) {
for (NSValue *num in _linesArray) {
CGPoint pt = [num CGPointValue];
double y = pt.y;
double x = pt.x;
//加10的原因是:最长线条是滚轮宽度减20,所以加上10,以达到刻度线居中的目的。
CGContextMoveToPoint(ctx, x, y+10);
CGContextAddLineToPoint(ctx,x,_layerLength+10 - y);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetStrokeColorWithColor(ctx,[UIColor whiteColor].CGColor);
CGContextStrokePath(ctx);
}
}
}
重写Control的三个系统方法:
/*!
@method 重写control的系统方法。
@abstract control开始触碰。
@discussion 开始触碰滚轮的方法,记录下触碰点。
@param touch 触碰点
@param event 事件
@result 返回布尔值
*/
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
_lastTouchPoint = [touch locationInView:self];
if (CGRectContainsPoint(self.layer.bounds, _lastTouchPoint)) {
[self.layer setNeedsDisplay];
return YES;
}
return NO;
}
/*!
@method 重写control的系统方法。
@abstract control持续触碰。
@discussion 持续触碰滚轮的方法,根据触碰点计算滚动距离。
@param touch 触碰点
@param event 事件
@result 返回布尔值
*/
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint pt = [touch locationInView:self];
CGFloat temLength = pt.x - _lastTouchPoint.x;
float radiuDelta = temLength/_kRadius;
self.value = temLength;
_scroledRange += temLength;
NSLog(@"aaa==%f",temLength);
_lastTouchPoint = pt;
if (_scroledRange < 0) {
_scroledRange =_kRadius + _scroledRange;
}
if (_scroledRange > _kRadius) {
_scroledRange =_scroledRange - _kRadius;
}
// 有效滚动才重置layer
if (radiuDelta != 0) {
[self resetLinesArray];
[self.layer setNeedsDisplay];
}
// 设置触发事件
[self sendActionsForControlEvents:UIControlEventValueChanged];
return YES;
}
/*!
@method 重写control的系统方法。
@abstract control结束触碰。
@discussion 结束触碰滚轮的方法。
@param touch 触碰点
@param event 事件
*/
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[self.layer setNeedsDisplay];
// 设置触发事件
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
使用方法:在ViewController中调用如下代码:
DCRotatingWheel *control = [[DCRotatingWheel alloc]initWithFrame:CGRectMake(40, 340, self.view.frame.size.width-80, 50)];
[control addTarget:self action:@selector(onControlTouchDown:) forControlEvents:UIControlEventTouchDown];
[control addTarget:self action:@selector(onControlTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[control addTarget:self action:@selector(onControlValueChange:) forControlEvents:UIControlEventValueChanged];
[self.view addSubview:control];
二、自定义视频播放器

1.创建AVPlayer:
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"xiujian2" ofType:@"mp4"];
NSURL *url = [NSURL fileURLWithPath:filePath];
self.item = [[AVPlayerItem alloc]initWithURL:url];
self.player = [[AVPlayer alloc]initWithPlayerItem:_item];
AVPlayerLayer *avLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
avLayer.frame = CGRectMake(0, 70, self.view.frame.size.width, 180);
avLayer.videoGravity = AVLayerVideoGravityResizeAspect;
[self.view.layer addSublayer:avLayer];
2.注册三个通知:
// 注册观察者,观察status属性
[_item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 观察缓冲进度
[_item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
3.观察播放进度及缓冲进度:
// 观察播放进度
- (void)monitoringPlayBack:(AVPlayerItem *)item {
__weak typeof(self)WeekSelf = self;
// 播放进度, 每秒执行30次, CMTime 为30分之一秒
_playTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 30.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
// 当前播放秒
float currentPlayTime = (double)item.currentTime.value/ item.currentTime.timescale;
// 更新播放进度Slider
[WeekSelf updateVideoSlider:currentPlayTime];
}];
}
// 更新滑条
- (void)updateVideoSlider:(float)currentTime {
self.mp4Slider.value = currentTime;
self.startTime.text = [self convertTime:currentTime];
}
// 已缓冲进度
- (NSTimeInterval)availableDurationRanges {
NSArray *loadedTimeRanges = [_item loadedTimeRanges]; // 获取item的缓冲数组
// CMTimeRange 结构体 start duration 表示起始位置 和 持续时间
CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue]; // 获取缓冲区域
float startSeconds = CMTimeGetSeconds(timeRange.start);
float durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds; // 计算总缓冲时间 = start + duration
return result;
}
4.观察item的播放状态:
// 观察status属性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status = [[change objectForKey:@"new"]integerValue];
// 准备播放
if (status == AVPlayerStatusReadyToPlay) {
CMTime duration = _item.duration;
NSLog(@"sssss%.2f", CMTimeGetSeconds(duration));
// 设置视频时间
[self setMaxDuration:CMTimeGetSeconds(duration)];
[self.player play];
}// 播放视频失败
else if (status == AVPlayerStatusFailed)
{
NSLog(@"AVPlayerStatusFailed");
}
else {
NSLog(@"AVPlayerStatusUnknown");
}
// 缓冲进度
}else if ([keyPath isEqualToString:@"loadedTimeRanges"]){
NSTimeInterval timeInterval = [self availableDurationRanges];
CGFloat totalDuration = CMTimeGetSeconds(_item.duration); // 总时间
[self.progressView setProgress:timeInterval / totalDuration animated:YES];
}
}
5.慢放滚轮或Slider控制播放进度的方法:
- (IBAction)ValueChange:(id)sender {
[self.player pause];
_play = NO;
[self setPlayBtnImage];
CMTime changeTime = CMTimeMakeWithSeconds(self.mp4Slider.value,1.0);
NSLog(@"%.2f", self.mp4Slider.value);
// seekToTime:控制视频跳转到指定时间播放,慢放滚轮也用相同的方法。
[_item seekToTime:changeTime completionHandler:^(BOOL finished) {
// [self.player play];
}];
}
需要注意的是,注册了通知,最后不要忘记要移除通知。其实这个播放器自定义的也不够完全,什么多倍播放,全屏处理,滑动屏幕快进,调节音量,调节屏幕亮度,调节分辨率什么的,都暂时还未处理。我做到这里就停了是因为我已实现慢放滚轮的功能,至于滚轮要添加到哪里,怎么用,可按照自己的需求添加。以上这些功能也不是做不出来,后期有时间我会慢慢加上。
最后说一下,文中关于慢放滚轮的设计思路,第4点我写的是暂无。关于需求所提到的“惯性”,我暂时还没有思路,不知道要怎么实现。要完全模拟滚轮特性,这惯性是必不可少的!如果各位有好的思路,还望不吝赐教。
本文于3月21日下午再次编辑,补充慢放滚轮惯性的设计思路
4.在滚轮将要结束滑动时,判断滑动距离向左或向右超出某一范围值时,设置定时器,让其持续滚动一小段距离。
相关代码:
/*!
@method 重写control的系统方法。
@abstract control结束触碰。
@discussion 结束触碰滚轮的方法。
@param touch 触碰点
@param event 事件
*/
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
self.value = _temLength;
[self.layer setNeedsDisplay];
// 设置触发事件
[self sendActionsForControlEvents:UIControlEventValueChanged];
if (_temLength > 0 && _temLength < 10 ) {
return;
}
if (_temLength < 0 && _temLength >-10) {
return;
}
_timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(onTimerEvent) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop]addTimer:_timer forMode:NSDefaultRunLoopMode];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_timer invalidate];
_timer = nil;
});
}
- (void)onTimerEvent
{
// 向左惯性滑动
if (_temLength < 0) {
_temLength -= 0.5;
}// 向右惯性滑动
else if (_temLength > 0)
{
_temLength += 0.5;
}
_scroledRange += _temLength;
if (_scroledRange < 0) {
_scroledRange =self.frame.size.width/2 + _scroledRange;
}
if (_scroledRange > self.frame.size.width/2) {
_scroledRange =_scroledRange - self.frame.size.width/2;
}
[self resetLinesArray];
[self.layer setNeedsDisplay];
// 设置代理方法,将每一时刻的值传出去
[_delegate onDCRotatingWheelDelegateInertanceEventWithValue:_temLength];
}
补充的代理方法:

具体使用方法:在主控制器中设置代理,并实现代理方法:
/*!
@method 慢放滚轮的惯性代理方法。
@abstract 慢放滚轮的惯性代理方法。
@param value 每次滚动的值
*/
- (void)onDCRotatingWheelDelegateInertanceEventWithValue:(float)value
{
float currentTime = self.mp4Slider.value+(value*0.1 );// 滚轮的拨动距离乘以系数,来控制进度(0.5)
if (currentTime > 0) {
CMTime changeTime = CMTimeMakeWithSeconds(currentTime,1.0);
[self updateVideoSlider:currentTime];
NSLog(@"%.2f", self.mp4Slider.value);
[_item seekToTime:changeTime completionHandler:^(BOOL finished) {
}];
}
}
转载请注明出处