Jul 8, 2019

Adding scrubbing support to video playback

More and more apps these days embed video content into its user experience. Often they will play automatically when shown and have the ability to play and pause the video when tapping on it. It seems this is becoming a standard practice for videos. But limiting interaction to just play and pause always seems a bit too restrictive to the user. So lets see how we can enable the user to scrub a video backwards and forwards when panning across it.

We start by adding an AVPlayerLayer and a UIPanGestureRecognizer to our view.

// video layer that will stream the url private let videoLayer = AVPlayerLayer() // pan gesture used for scrubbing private let panGesture = UIPanGestureRecognizer() // setup view override init(frame: CGRect) { super.init(frame: frame) // self backgroundColor = .black // setup videoLayer videoLayer.videoGravity = .resizeAspect layer.addSublayer(videoLayer) // setup panGesture panGesture.minimumNumberOfTouches = 1 panGesture.maximumNumberOfTouches = 1 panGesture.cancelsTouchesInView = true panGesture.addTarget(self, action: #selector(didScrub(recognizer:))) addGestureRecognizer(panGesture) } override func layoutSublayers(of layer: CALayer) { super.layoutSublayers(of: layer) // resize videoLayer frame to fit bounds videoLayer.frame = bounds }

Since it is not possible to limit the UIPanGestureRecognizer to only take inputs when panning horizontally, we must override its delegate method gestureRecognizerShouldBegin() and prevent it from beginning when the user pans vertically.

override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer == panGesture { // only allow pan gesture to begin when panning horizontally let translation = panGesture.translation(in: self) return abs(translation.x) >= 0 && abs(translation.x) > abs(translation.y) } return super.gestureRecognizerShouldBegin(gestureRecognizer) }

Now the important part is actually handling the pan gesture when receiving it. There are three important stages we need to consider to make scrubbing work as intended:

When the scrubbing begins, we must pause the video and save the current CMTime value of the AVPlayerLayer . This is important because we want to start scrubbing the video from the current position of the playback. Whenever the translated position of the users finger on the AVPlayerLayer changes, the point to which it must seek should change accordingly. For this we use the previously saved scrubbingBeginTime to get the percentage of the videos completed playback and then calculate the percentage of the translated position in the AVPlayerLayer from left. Adding these two together and limiting the value to >= 0 & <= 1.0 lets us easily calculate the new CMTime value by using CMTimeMakeWithSeconds(, preferredTimescale:) , which we than pass on to the seek(to:, toleranceBefore:, toleranceAfter:) method of the AVPlayerLayer . This method actually jumps the videos position the the new CMTime we just calculated. After the pan has ended or is cancelled, the video should resume playback. It will automatically do this from the position we seeked to in the previous step.

// time when scrubbing began private var scrubbingBeginTime: CMTime? @objc private func didScrub(recognizer: UIPanGestureRecognizer) { guard videoLayer.isReadyForDisplay == true, let player = videoLayer.player, let currentItem = player.currentItem else { return } switch recognizer.state { case .possible: // nothing to do here break case .began: // pause playback when user begins panning videoLayer.player?.pause() // set time scrubbing began scrubbingBeginTime = currentItem.currentTime() case .changed: guard let scrubbingBeginTime = scrubbingBeginTime else { return } let totalSeconds = currentItem.duration.seconds // translate point of pan in view let point = recognizer.translation(in: self) let scrubbingBeginPercent = Double(scrubbingBeginTime.seconds/totalSeconds) // calculate percentage of point in view var percent = Double(point.x/bounds.width) percent += scrubbingBeginPercent if percent < 0 { percent = 0 } else if percent > 1.0 { percent = 1.0 } // calculate time to seek to in video timeline let seconds = Float64(percent * totalSeconds) let time = CMTimeMakeWithSeconds(seconds, preferredTimescale: currentItem.duration.timescale) player.seek(to: time, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) case .ended, .cancelled, .failed: // reset scrubbing begin time scrubbingBeginTime = nil // resume playback after user stops panning videoLayer.player?.play() @unknown default: break } }

That’s it. As you can see, adding scrubbing to a video is fairly straight forward and made easy with the API the AVPlayerLayer exposes to us.

You can view the complete code here or experience how it works in my app Monocle.

Language: Swift 5.0 · Written on: iPad Pro

Please enable JavaScript to view the comments powered by Disqus.