Jumpy UISlider when scrubbing - Using UISlider with AVPlayer
Asked Answered
L

5

10

I am using AvPlayer and am trying to set up a slider to allow scrubbing of audio files. Im having a problem with the slider jumping all over the place when its selected. It then goes back to the origin position for a second before going back to the location it was dragged to.

You cant see my cursor on the Gif, but the smooth elongated drags are me moving the knob, then the quick whips are the slider misbehaving.

enter image description here

Ive spent hours googling and combing through Stack Overflow and cant figure out what I'm doing wrong here, a lot of similar questions are quite old and in ObjC.

This is the section of code i think is responsible for the problem, it does handle the event of the slider being moved: Ive tried it without the if statement also and didn't see a different result.

@IBAction func horizontalSliderActioned(_ sender: Any) {

    horizontalSlider.isContinuous = true

    if self.horizontalSlider.isTouchInside {

        audioPlayer?.pause()
            let seconds : Int64 = Int64(horizontalSlider.value)
                let preferredTimeScale : Int32 = 1
                    let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
                        audioPlayerItem?.seek(to: seekTime)
                            audioPlayer?.play()

    } else {

        let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
            let seconds : Float64 = CMTimeGetSeconds(duration)
                self.horizontalSlider.value = Float(seconds)
    }
}

I will include my entire class below for reference.

import UIKit
import Parse
import AVFoundation
import AVKit


class PlayerViewController: UIViewController, AVAudioPlayerDelegate {

    @IBOutlet var horizontalSlider: UISlider!

    var selectedAudio: String!

    var audioPlayer: AVPlayer?
    var audioPlayerItem: AVPlayerItem?

    var timer: Timer?


    func getAudio() {

        let query = PFQuery(className: "Part")
                query.whereKey("objectId", equalTo: selectedAudio)
                    query.getFirstObjectInBackground { (object, error) in

                if error != nil || object == nil {
                    print("The getFirstObject request failed.")

                } else {
                    print("There is an object now get the Audio. ")

                        let audioFileURL = (object?.object(forKey: "partAudio") as! PFFile).url
                            self.audioPlayerItem = AVPlayerItem(url: NSURL(string: audioFileURL!) as! URL)
                                self.audioPlayer = AVPlayer(playerItem: self.audioPlayerItem)
                                    let playerLayer = AVPlayerLayer(player: self.audioPlayer)
                                        playerLayer.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
                                            self.view.layer.addSublayer(playerLayer)

                        let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
                            let seconds : Float64 = CMTimeGetSeconds(duration)
                                let maxTime : Float = Float(seconds)
                                    self.horizontalSlider.maximumValue = maxTime

                        self.audioPlayer?.play()

                        self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PlayerViewController.audioSliderUpdate), userInfo: nil, repeats: true)
                }
            }
    }


    @IBOutlet var playerButton: UIButton!


    func playerButtonTapped() {

        if audioPlayer?.rate == 0 {
            audioPlayer?.play()
                self.playerButton.setImage(UIImage(named: "play"), for: UIControlState.normal)

        } else {
            audioPlayer?.pause()
                self.playerButton.setImage(UIImage(named: "pause"), for: UIControlState.normal)
        }

    }


    override func viewDidLoad() {
        super.viewDidLoad()

        horizontalSlider.minimumValue = 0
            horizontalSlider.value = 0

                self.playerButton.addTarget(self, action: #selector(PlayerViewController.playerButtonTapped), for: UIControlEvents.touchUpInside)

                    getAudio()
    }



    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, selector: #selector(PlayerViewController.finishedPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.audioPlayerItem)

    }



    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillAppear(animated)
            // remove the timer
                self.timer?.invalidate()
                    // remove the observer when leaving page
                        NotificationCenter.default.removeObserver(audioPlayer?.currentItem! as Any)
        }



    func finishedPlaying() {

        // need option to play next track

        self.playerButton.setImage(UIImage(named: "play"), for: UIControlState.normal)

            let seconds : Int64 = 0
                let preferredTimeScale : Int32 = 1
                    let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)

                        audioPlayerItem!.seek(to: seekTime)
    }

    @IBAction func horizontalSliderActioned(_ sender: Any) {

        horizontalSlider.isContinuous = true

        if self.horizontalSlider.isTouchInside {

            audioPlayer?.pause()
                let seconds : Int64 = Int64(horizontalSlider.value)
                    let preferredTimeScale : Int32 = 1
                        let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
                            audioPlayerItem?.seek(to: seekTime)
                                audioPlayer?.play()

        } else {

            let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
                let seconds : Float64 = CMTimeGetSeconds(duration)
                    self.horizontalSlider.value = Float(seconds)
        }
    }



    func audioSliderUpdate() {

        let currentTime : CMTime = (self.audioPlayerItem?.currentTime())!
            let seconds : Float64 = CMTimeGetSeconds(currentTime)
                let time : Float = Float(seconds)
                    self.horizontalSlider.value = time
    }

}
Lorrianelorrie answered 20/1, 2017 at 4:13 Comment(0)
L
6

Swift 5, Xcode 11

I faced the same issue, it was apparently periodicTimeObserver which was causing to return incorrect time which caused lag or jump in the slider. I solved it by removing periodic time observer when the slider was changing and adding it back when seeking completion handler was called.

@objc func sliderValueChanged(_ playbackSlider: UISlider, event: UIEvent){

    let seconds : Float = Float(playbackSlider.value)
    let targetTime:CMTime = CMTimeMake(value: Int64(seconds), timescale: 1)


    if let touchEvent = event.allTouches?.first {
        switch touchEvent.phase {
        case .began:
            // handle drag began
            //Remove observer when dragging is in progress
            self.removePeriodicTimeObserver()


            break
        case .moved:
            // handle drag moved

            break
        case .ended:
            // handle drag ended

            //Add Observer back when seeking got completed
            player.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] (value) in
                self?.addTimeObserver()
            }

            break
        default:
            break
        }
    }
}
Lachellelaches answered 1/10, 2019 at 11:57 Comment(6)
@kedarSurekar I have tried your solution, but still I am getting jumping slider while doing drag forward, Can you help me?Ornie
@KedarSurekar can you pls check this one #62775334Ornie
@KedarSurekar I have fixed the issue. ThanksOrnie
Nice to hear....what was the issue?? @AnilkumariOS-ReactNativeLachellelaches
@KedarSurekhar I have answer to my question, You can check there I updated answerOrnie
This is definitely the way to do it, it 100% works. It stopped the jumpy slider issue. I'm glad I came across this answerZinnes
R
5

you need to remove observers and invalidate timers as soon as user selects the thumb on slider and add them back again when dragging is done

to do add targets like this where you load your player:

    mySlider.addTarget(self, 
action: #selector(PlayerViewController.mySliderBeganTracking(_:)),
forControlEvents:.TouchDown)

    mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderEndedTracking(_:)),
forControlEvents: .TouchUpInside)

   mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderEndedTracking(_:)),
forControlEvents: .TouchUpOutside )

and remove observers and invalidate timers in mySliderBeganTracking then add observers in mySliderEndedTracking for better control on what happens in your player write 2 functions : addObservers and removeObservers and call them when needed

Rasure answered 14/2, 2017 at 8:5 Comment(7)
Hey Mohy, have you solved the problem? I have come across it as well.Osborne
Yes @JamesRao, it was caused by my observers and timers(for show/hiding toolbars automatically after few seconds), I'm removing them every time user touches the slider's thumb image, and add them back when user releases the slider :)Rasure
I finally made it by doing Pause at touch down, and Play at touch up. Thank you so much! :)Osborne
@JamesRao Even I am getting same issue, I followed this solution, But still get same issue, Can you check this #62775334Ornie
@JamesRao still I am getting same issue, can you check this? #62775334Ornie
@Rasure still I am getting same issue, can you check this? #62775334Ornie
This is how you're supposed to do itZinnes
G
1

Make sure to do the following:

  1. isContinuous for the slider is NOT set to false.
  2. Pause the player before seeking.
  3. Seek to the position and use the completion handler to resume playing.

Example code:

@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {

    let roundedValue = sender.value.rounded()
    guard let touchEvent = event.allTouches?.first else { return }
    
    switch touchEvent.phase {
        
        case .began:
            PlayerManager.shared.pause()
            
        case .moved:
            print("Slider moved")

        case .ended:
            PlayerManager.shared.seek(to: roundedValue, playAfterSeeking: true)
        
        default: ()
    }
}

And here is the function for seeking:

func seek(to: Float, playAfterSeeking: Bool) {
    player?.seek(to: CMTime(value: CMTimeValue(to), timescale: 1), completionHandler: { [weak self] (status) in
        if playAfterSeeking {
            self?.play()
        }
    })
}
Golter answered 13/10, 2020 at 15:26 Comment(0)
S
0

Try using the time slider value like below:

 @IBAction func timeSliderDidChange(_ sender: UISlider) {
            AVPlayerManager.sharedInstance.currentTime = Double(sender.value)
        }

var currentTime: Double {
        get {
            return CMTimeGetSeconds(player.currentTime())
        }

        set {
            if self.player.status == .readyToPlay {
                let newTime = CMTimeMakeWithSeconds(newValue, 1)
                player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { ( _ ) in
                    self.updatePlayerInfo()
                }
            }
        }
    }

and pass the value of slider when user release the slider, also don't update the slider value of current playing while user interaction happening on the slider

Skycap answered 18/2, 2017 at 5:44 Comment(0)
A
0

This is a temporary solution for me, I observed that the rebound is only once, so I set an int value isSeekInProgress:

  1. When sliderDidFinish, isSeekInProgress = 0
  2. In reply to avplayer time change:
    if (self.isSeekInProgress > 1) {
        float sliderValue = 1.f / (self.slider.maximumValue - self.slider.minimumValue) * progress;
        //    if (sliderValue > self.slider.value ) {
        self.slider.value = sliderValue;
    }else {
        self.isSeekInProgress += 1;
    }

Angular answered 24/10, 2021 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.