UISlider as audio seekbar jumping to maximum value when changed - ios

I am using a UISlider as a seek bar for audio and it works great for adjusting to change position in the track if it is not animated. If it's animated it works great and tracks along the bar in time with the track perfectly but if you try and adjust it while the animation is active, it jumps to the maximum value of the bar. I assume there is a conflict between the two processes but I'm struggling to work out a fix.
func changeProgressBar() {
let trackLength = Float(AudioService.shared.playerItem?.duration.seconds ?? 0)
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true){_ in
let currentTime = Float(AudioService.shared.player?.currentTime().seconds ?? 0)
let sliderPosition = currentTime / (trackLength / 100)
self.progressBar.setValue(sliderPosition, animated: true)
print("the current time is", currentTime)
print("the slider position is", sliderPosition)
}
}
#IBAction func progressBarValueChanged(_ sender: UISlider) {
let trackLength = AudioService.shared.playerItem?.duration.seconds ?? 0
let sliderValueFloat = progressBar.value * 100.00
let sliderValueDouble = Double(sliderValueFloat)
let targetTime = (trackLength / 100 * sliderValueDouble)
let targetTimeActual = CMTimeMake(value: Int64(targetTime), timescale: 1)
AudioService.shared.player!.seek(to: targetTimeActual)
}
I have buttons that add or subtract 30 seconds to skip forward or back in the track and they work fine even when the animation is active.
#IBAction func plus30Secs(_ sender: UIButton) {
let currentTime = Float(AudioService.shared.player?.currentTime().seconds ?? 0)
let seekTime = currentTime + 30
let seekTimeActual = CMTimeMake(value: Int64(seekTime), timescale: 1)
AudioService.shared.player!.seek(to: seekTimeActual)
}
#IBAction func minus30Secs(_ sender: UIButton) {
let currentTime = Float(AudioService.shared.player?.currentTime().seconds ?? 0)
let seekTime = currentTime - 30
let seekTimeActual = CMTimeMake(value: Int64(seekTime), timescale: 1)
AudioService.shared.player!.seek(to: seekTimeActual)
}

ok, i have fixed it.
The issue was i had progressBar.maximumValue = 100 meaning that my progressBar.value * 100.00 was giving a value 100 times what it should have been and as such a value beyond the end of the track. I removed the * 100.00 and now it works great.

Related

Swift - How to get the current position of AVAudioPlayerNode while it's looping?

I have an AVAudioPlayerNode looping a segment of a song:
audioPlayer.scheduleBuffer(segment, at: nil, options:.loops)
I want to get current position of the song while it's playing. Usually, this is done by calculating = currentFrame / audioSampleRate
where
var currentFrame: AVAudioFramePosition {
guard let lastRenderTime = audioPlayer.lastRenderTime,
let playerTime = audioPlayer.playerTime(forNodeTime: lastRenderTime) else {
return 0
}
return playerTime.sampleTime
}
However, when the loop ends and restarts, the currentFrame does not restart. But it still increases which makes currentFrame / audioSampleRate incorrect as the current position.
So what is the correct way to calculate the current position?
Good old modulo will do the job:
public var currentTime: TimeInterval {
guard let nodeTime = player.lastRenderTime,
let playerTime = player.playerTime(forNodeTime: nodeTime) else {
return 0
}
let time = (Double(playerTime.sampleTime) / playerTime.sampleRate)
.truncatingRemainder(dividingBy: Double(file.length) / Double(playerTime.sampleRate))
return time
}

Swift: Trying to control time in AVAudioPlayerNode using UISlider

I'm using an AVAudioPlayerNode attached to an AVAudioEngine to play a sound.
to get the current time of the player I'm doing this:
extension AVAudioPlayerNode {
var currentTime: TimeInterval {
get {
if let nodeTime: AVAudioTime = self.lastRenderTime, let playerTime: AVAudioTime = self.playerTime(forNodeTime: nodeTime) {
return Double(playerTime.sampleTime) / playerTime.sampleRate
}
return 0
}
}
}
I have a slider that indicates the current time of the audio. When the user changes the slider value, on .ended event I have to change the current time of the player to that indicated in the slider.
To do so:
extension AVAudioPlayerNode {
func seekTo(value: Float, audioFile: AVAudioFile, duration: Float) {
if let nodetime = self.lastRenderTime{
let playerTime: AVAudioTime = self.playerTime(forNodeTime: nodetime)!
let sampleRate = self.outputFormat(forBus: 0).sampleRate
let newsampletime = AVAudioFramePosition(Int(sampleRate * Double(value)))
let length = duration - value
let framestoplay = AVAudioFrameCount(Float(playerTime.sampleRate) * length)
self.stop()
if framestoplay > 1000 {
self.scheduleSegment(audioFile, startingFrame: newsampletime, frameCount: framestoplay, at: nil,completionHandler: nil)
}
}
self.play()
}
However, my function seekTo is not working correctly(I'm printing currentTime before and after the function and it shows always a negative value ~= -0.02). What is the wrong thing I'm doing and can I find a simpler way to change the currentTime of the player?
I ran into same issue. Apparently the framestoplay was always 0, which happened because of sampleRate. The value for playerTime.sampleRate was always 0 in my case.
So,
let framestoplay = AVAudioFrameCount(Float(playerTime.sampleRate) * length)
must be replaced with
let framestoplay = AVAudioFrameCount(Float(sampleRate) * length)

update control center playback seek when player view controller of slider value changed

I got to change the slider value when the control center of change playback media position is changed...
commandCenter.changePlaybackPositionCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
let event = event as! MPChangePlaybackPositionCommandEvent
let time = CMTime(seconds: event.positionTime, preferredTimescale: self.player.currentTime().timescale)
self.player.seek(to: time)
return .success;
}
and
#IBAction func handleSliderChange(_ sender: UISlider) {
let seconds : Int64 = Int64(sender.value)
let targetTime:CMTime = CMTimeMake(seconds, 1)
myPlayer.player.seek(to: targetTime)
}
#IBAction func sliderTapped(_ sender: UILongPressGestureRecognizer) {
if let slider = sender.view as? UISlider {
if slider.isHighlighted { return }
let point = sender.location(in: slider)
let percentage = Float(point.x / slider.bounds.width)
let delta = percentage * (slider.maximumValue - slider.minimumValue)
let value = slider.minimumValue + delta
slider.setValue(value, animated: false)
let seconds : Int64 = Int64(value)
let targetTime: CMTime = CMTimeMake(seconds, 1)
self.myPlayer.player.seek(to: targetTime)
}
}
however, in inverse method, I have no idea what to do. I want to update current time when the slider value is changed.

AVPlayer seektotime with Pangesturerecognizer

I'm trying to use seektotime with Pangesture recognizer.But its not seeking as expected.
let totalTime = self.avPlayer.currentItem!.duration
print("time: \(CMTimeGetSeconds(totalTime))")
self.avPlayer.pause()
let touchDelta = swipeGesture.translationInView(self.view).x / CGFloat(CMTimeGetSeconds(totalTime))
let currentTime = CMTimeGetSeconds((avPlayer.currentItem?.currentTime())!) + Float64(touchDelta)
print(currentTime)
if currentTime >= 0 && currentTime <= CMTimeGetSeconds(totalTime) {
let newTime = CMTimeMakeWithSeconds(currentTime, Int32(NSEC_PER_SEC))
print(newTime)
self.avPlayer.seekToTime(newTime)
}
What I'm doing wrong in here ?
Think about what's happening in this line here:
let touchDelta = swipeGesture.translationInView(self.view).x / CGFloat(CMTimeGetSeconds(totalTime))
You're dividing pixels (the translation in just the x-axis) by time. This really isn't a "delta" or absolute difference. It's a ratio of sorts. But it's not a ratio that has any meaning. Then you're getting your new currentTime by just added this ratio to the previous currentTime, so you're adding pixels per seconds to pixels, which doesn't give a logical or useful number.
What we need to do is take the x-axis translation from the gesture and apply a scale (which is a ratio) to it in order to get a useful number of seconds to advance/rewind the AVPlayer. The x-axis translation is in pixels so we'll need a scale that describes seconds per pixels and multiple the two in order to get our number of seconds. The proper scale is the ratio between the total number of seconds in the video and the total number of pixels that the user can move through in the gesture. Multiplying pixels times (seconds divided by pixels) gives us a number in seconds. In pseudocode:
scale = totalSeconds / totalPixels
timeDelta = translation * scale
currentTime = oldTime + timeDelta
So I would rewrite your code like this:
let totalTime = self.avPlayer.currentItem!.duration
print("time: \(CMTimeGetSeconds(totalTime))")
self.avPlayer.pause()
// BEGIN NEW CODE
let touchDelta = swipeGesture.translationInView(self.view).x
let scale = CGFloat(CMTimeGetSeconds(totalTime)) / self.view.bounds.width
let timeDelta = touchDelta * scale
let currentTime = CMTimeGetSeconds((avPlayer.currentItem?.currentTime())!) + Float64(timeDelta)
// END NEW CODE
print(currentTime)
if currentTime >= 0 && currentTime <= CMTimeGetSeconds(totalTime) {
let newTime = CMTimeMakeWithSeconds(currentTime, Int32(NSEC_PER_SEC))
print(newTime)
self.avPlayer.seekToTime(newTime)
}
I have same issue, then i create the UISlider and set the action method is given below,
declare AVPlayer is var playerVal = AVPlayer()
#IBAction func sliderAction(sender: UISlider) {
playerVal.pause()
displayLink.invalidate()
let newTime:CMTime = CMTimeMakeWithSeconds(Double(self.getAudioDuration() as! NSNumber) * Double(sender.value), playerVal.currentTime().timescale)
playerVal.seekToTime(newTime)
updateTime()
playerVal.play()
deepLink()
}
And another method is,
func updateTime() {
let currentTime = Float(CMTimeGetSeconds(playerItem1.currentTime()))
let minutes = currentTime/60
let seconds = currentTime - minutes * 60
let maxTime = Float(self.getAudioDuration() as! NSNumber)
let maxminutes = maxTime / 60
let maxseconds = maxTime - maxminutes * 60
startValue.text = NSString(format: "%.2f:%.2f", minutes,seconds) as String
stopValue.text = NSString(format: "%.2f:%.2f", maxminutes,maxseconds) as String
}
I have used CADisplayLink and declare var displayLink = CADisplayLink(), its used continue(automatically) playing audios. code is
func deepLink() {
displayLink = CADisplayLink(target: self, selector: ("updateSliderProgress"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
}
func updateSliderProgress(){
let progress = Float(CMTimeGetSeconds(playerVal.currentTime())) / Float(self.getAudioDuration() as! NSNumber)
sliderView.setValue(Float(progress), animated: false)
}
if you see this above answer, you have get idea, hope its helpful

How do I get current playing time and total play time in AVPlayer?

Is it possible get playing time and total play time in AVPlayer? If yes, how can I do this?
You can access currently played item by using currentItem property:
AVPlayerItem *currentItem = yourAVPlayer.currentItem;
Then you can easily get the requested time values
CMTime duration = currentItem.duration; //total time
CMTime currentTime = currentItem.currentTime; //playing time
Swift 5:
if let currentItem = player.currentItem {
let duration = CMTimeGetSeconds(currentItem.duration)
let currentTime = CMTimeGetSeconds(currentItem.currentTime())
print("Duration: \(duration) s")
print("Current time: \(currentTime) s")
}
_audioPlayer = [self playerWithAudio:_audio];
_observer =
[_audioPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 2)
queue:dispatch_get_main_queue()
usingBlock:^(CMTime time)
{
_progress = CMTimeGetSeconds(time);
}];
Swift 3
let currentTime:Double = player.currentItem.currentTime().seconds
You can get the seconds of your current time by accessing the seconds property of the currentTime(). This will return a Double that represents the seconds in time. Then you can use this value to construct a readable time to present to your user.
First, include a method to return the time variables for H:mm:ss that you will display to the user:
func getHoursMinutesSecondsFrom(seconds: Double) -> (hours: Int, minutes: Int, seconds: Int) {
let secs = Int(seconds)
let hours = secs / 3600
let minutes = (secs % 3600) / 60
let seconds = (secs % 3600) % 60
return (hours, minutes, seconds)
}
Next, a method that will convert the values you retrieved above into a readable string:
func formatTimeFor(seconds: Double) -> String {
let result = getHoursMinutesSecondsFrom(seconds: seconds)
let hoursString = "\(result.hours)"
var minutesString = "\(result.minutes)"
if minutesString.characters.count == 1 {
minutesString = "0\(result.minutes)"
}
var secondsString = "\(result.seconds)"
if secondsString.characters.count == 1 {
secondsString = "0\(result.seconds)"
}
var time = "\(hoursString):"
if result.hours >= 1 {
time.append("\(minutesString):\(secondsString)")
}
else {
time = "\(minutesString):\(secondsString)"
}
return time
}
Now, update the UI with the previous calculations:
func updateTime() {
// Access current item
if let currentItem = player.currentItem {
// Get the current time in seconds
let playhead = currentItem.currentTime().seconds
let duration = currentItem.duration.seconds
// Format seconds for human readable string
playheadLabel.text = formatTimeFor(seconds: playhead)
durationLabel.text = formatTimeFor(seconds: duration)
}
}
With Swift 4.2, use this;
let currentPlayer = AVPlayer()
if let currentItem = currentPlayer.currentItem {
let duration = currentItem.asset.duration
}
let currentTime = currentPlayer.currentTime()
Swift 4
self.playerItem = AVPlayerItem(url: videoUrl!)
self.player = AVPlayer(playerItem: self.playerItem)
self.player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main, using: { (time) in
if self.player!.currentItem?.status == .readyToPlay {
let currentTime = CMTimeGetSeconds(self.player!.currentTime())
let secs = Int(currentTime)
self.timeLabel.text = NSString(format: "%02d:%02d", secs/60, secs%60) as String//"\(secs/60):\(secs%60)"
})
}
AVPlayerItem *currentItem = player.currentItem;
NSTimeInterval currentTime = CMTimeGetSeconds(currentItem.currentTime);
NSLog(#" Capturing Time :%f ",currentTime);
Swift:
let currentItem = yourAVPlayer.currentItem
let duration = currentItem.asset.duration
var currentTime = currentItem.asset.currentTime
Swift 5:
Timer.scheduledTimer seems better than addPeriodicTimeObserver if you want to have a smooth progress bar
static public var currenTime = 0.0
static public var currenTimeString = "00:00"
Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { timer in
if self.player!.currentItem?.status == .readyToPlay {
let timeElapsed = CMTimeGetSeconds(self.player!.currentTime())
let secs = Int(timeElapsed)
self.currenTime = timeElapsed
self.currenTimeString = NSString(format: "%02d:%02d", secs/60, secs%60) as String
print("AudioPlayer TIME UPDATE: \(self.currenTime) \(self.currenTimeString)")
}
}
Swift 4.2:
let currentItem = yourAVPlayer.currentItem
let duration = currentItem.asset.duration
let currentTime = currentItem.currentTime()
in swift 5+
You can query the player directly to find the current time of the actively playing AVPlayerItem.
The time is stored in a CMTime Struct for ease of conversion to various scales such as 10th of sec, 100th of a sec etc
In most cases we need to represent times in seconds so the following will show you what you want
let currentTimeInSecs = CMTimeGetSeconds(player.currentTime())

Resources