Search code examples
iosavplayeruipangesturerecognizeravplayerlayer

AVPlayer fast forward/backward skipping with pan gesture


I've been bashing my head with an annoying glitch for the past two days and was hoping somebody could shed some light.

Basic setup: I have an AVPlayerLayer with a player, a pan gesture recognizer in the view and I want the user to be able to swipe their fingers back and forth and the video would seek accordingly.

The catch: I would like if the user lifts their finger and places it again,anywhere on the screen for the fast forward or backward to resume at the exact same frame where it left off and continue progressing from there.

I've seen these two questions: Pan Gesture with AVPlayer and Pan to seek AVPlayer

I've tried Apple's suggestion here: https://developer.apple.com/library/content/qa/qa1820/_index.html as well, but the problem is that whenever I start a new pan gesture, the player jumps a few frames and then resumes.

My latest approach was to set the current time once the seek completion block finishes and then I try and append the new seek time to that.

Here is my setup:

self.item = [AVPlayerItem playerItemWithURL:resource];
self.player = [AVPlayer playerWithPlayerItem:self.item];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];

[self.view.layer addSublayer:self.playerLayer];
self.playerLayer.frame = self.view.bounds;

UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];

[self.view addGestureRecognizer:recognizer];

And my gesture recognizer handling:

- (void)swipe:(UIPanGestureRecognizer *)paramRecognizer
{
    switch(paramRecognizer.state) {
      //  case UIGestureRecognizerStateBegan:
        case UIGestureRecognizerStateChanged:
        {
            [self.player pause];
            
            CGPoint translation = [paramRecognizer translationInView:self.view];
            
            float horizontalTranslation = translation.x;
            
            float durationInSeconds = CMTimeGetSeconds(self.player.currentItem.asset.duration);
            
            //I'd like to be able to swipe across the whole view.
            float translationLimit = self.view.bounds.size.width;
            float minTranslation = 0;
            float maxTranslation = translationLimit;
            
            if (horizontalTranslation > maxTranslation) {
                horizontalTranslation = maxTranslation;
            }
            
            if (horizontalTranslation < minTranslation) {
                horizontalTranslation = minTranslation;
            }
            
        float timeToSeekTo = [self normalize:horizontalTranslation
                                 andMinDelta:minTranslation
                                 andMaxDelta:maxTranslation
                              andMinDuration:0
                              andMaxDuration:durationInSeconds];
            
            if(CMTIME_IS_VALID(self.currentTime)){
                float seconds = self.currentTime.value/self.currentTime.timescale;
        
                [self.player seekToTime:CMTimeMakeWithSeconds(seconds+timeToSeekTo, self.player.currentTime.timescale)
                        toleranceBefore:kCMTimeZero
                         toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished)
                 {
                     if(finished)
                    self.currentTime = self.player.currentItem.currentTime;
                }];
            }
            else
            {
                [self.player seekToTime:CMTimeMakeWithSeconds(timeToSeekTo,
                                                              self.player.currentTime.timescale) toleranceBefore:kCMTimeZero
                         toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
                             if(finished)
                    self.currentTime = self.player.currentItem.currentTime;
                }];
            }
            
        }
            break;
    }
}

The normalize method is this:

- (float)normalize:(float)delta
       andMinDelta:(float)minDelta
       andMaxDelta:(float)maxDelta
    andMinDuration:(float)minDuration
    andMaxDuration:(float)maxDuration
{
  float result = ((delta - minDelta) * (maxDuration - minDuration) / (maxDelta - minDelta) + minDuration);
  return result;
}

Any help would be extremely appreciated!


Solution

  • On UIGestureRecognizerStateBegan save CMTime through from AVPlayer then do your delta change panning, then on UIGestureRecognizerStateEnded just seek back to the original saved time?

    Just a note for smoother seeking do not pause the video, set the rate to 0.