自定義視訊播放器與慢放滾輪

XDChang發表於2018-07-10

受同學之邀,幫忙自定義一控制元件。需求是:開發慢放滾輪,用手指撥動實現幀級的慢速播放,滾輪可雙向撥動,其滾動具有慣性,滾動速度決定視訊播放的速度。需求很明朗,可我卻是一頭霧水。說實話,在此之前我還沒有自定義過視訊播放器,不懂怎麼實現‘幀級慢速播放’。並且滾輪這個東西自定義起來,我也是沒譜的。越是自己覺得陌生的東西,越是想辦法迴避,想著挑戰一下自己,就嘗試著做了起來。

要實現這個需求,首先滾輪是要自定義的。再說視訊播放器,如果要用別人寫好的視訊播放器框架,需要coder剛好提供了一個方法,專門控制視訊播放進度的。為了避免繁瑣,我基於系統的AVPlayer自定義了視訊播放器,實現了相關功能。DCVideoPlyer

介面效果圖

滾輪慢放效果圖
####一、自定義慢放滾輪

#####設計思路: 1.滾輪有三個狀態:開始觸碰、持續滾動、結束觸碰。基於這三個狀態考慮,決定在Control上自定義。

2.滾輪看起來得有立體的感覺,刻度線間距應該有變化,並且長度也應該有變化,達到近大遠小的效果。

3.滾輪上的刻度線是繞著圓心轉動的,想象著平面上是一個半圓,刻度線的長度與間距從小到大,又從大到小變化,你是不是想起了高中所學三角函式,這裡就是利用三角函式與反三角函式實現相關功能。

4.暫無

sin函式和cos函式
#####實現功能:

滾輪相關
實現滾輪刻度線的整體效果,其關鍵核心就是利用三角函式,計算出每條線條的起始點。要驗證函式的正確性,可將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];
複製程式碼

####二、自定義視訊播放器

AVPlayer相關
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) {
            
        }];
    } 
}
複製程式碼

DCVideoPlyer

轉載請註明出處

相關文章