Search code examples
iosswiftavplayeruislider

UISlider jumps when updating for AVPlayer


I try to implement simple player with UISlider to indicate at what time is current audio file.

enter image description here

In code I have added two observers:

    slider.rx.value.subscribe(onNext: { value in
        let totalTime = Float(CMTimeGetSeconds(self.player.currentItem!.duration))
        let seconds = value * totalTime
        let time = CMTime(seconds: Double(seconds), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        self.player.seek(to: time)
        }).disposed(by: bag)
    let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
    player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
        self?.updateSlider(with: time)
    }

with one private function:

private func updateSlider(with time: CMTime) {
    let currentTime = CMTimeGetSeconds(time)
    var totalTime = CMTimeGetSeconds(player.currentItem!.duration)
    if totalTime.isNaN {
        totalTime = 0
    }
    startLabel.text = Int(currentTime).descriptiveDuration
    endLabel.text = Int(totalTime).descriptiveDuration
    slider.value = Float(currentTime / totalTime)
}

When audio plays, everything is fine and slider is pretty much updated. The problem occurs when I try to move slider manually while audio is playing, then it jumps. Why?

Update

I know why actually. Because I update it twice: manually and from player observer, but how to prevent from this behaviour?


Solution

  • One simple way to go about this would be to prevent addPeriodicTimeObserver from calling self?.updateSlider(with: time) when the slider is being touched.

    This can be determined via the UISliders isTracking property:

    isTracking

    A Boolean value indicating whether the control is currently tracking touch events.

    While tracking of a touch event is in progress, the control sets the value of this property to true. When tracking ends or is cancelled for any reason, it sets this property to false.

    Ref: https://developer.apple.com/documentation/uikit/uicontrol/1618210-istracking

    This is present in all UIControl elements which you can use in this way:

    player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
        //check if slider is being touched/tracked
        guard self?.slider.isTracking == false else { return }
    
        //if slider is not being touched, then update the slider from here
        self?.updateSlider(with: time)
    }
    

    Generic Example:

    @IBOutlet var slider: UISlider!
    //...
    func startSlider() {
        slider.value = 0
        slider.maximumValue = 10
    
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] (timer) in
            print("Slider at: \(self?.slider.value)")
            guard self?.slider.isTracking == false else { return }
            self?.updateSlider(to: self!.slider.value + 0.1)
        }
    }
    
    private func updateSlider(to value: Float) {
        slider.value = value
    }
    

    I'm sure there are other (better) ways out there but I haven't done much in RxSwift (yet).
    I hope this is good enough for now.