AVPlayer - Fast Backward / Forward Stream - iphone

This is my code in the viewDidLoad :
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:#"http://groove.wavestreamer.com:7321/listen.pls?sid=1"]];
[playerItem addObserver:self forKeyPath:#"timedMetadata" options:NSKeyValueObservingOptionNew context:nil];
music = [[AVPlayer playerWithPlayerItem:playerItem] retain];
[music play];
My question:
How can I create a button, that fast-forwards / fast-backwards the stream 5 seconds, when it´s pressed?
Thank you for your answers... :)
EDIT: How can I add to my current time...
CMTime currentTime = music.currentTime;
...the 5 seconds?

In Swift,
fileprivate let seekDuration: Float64 = 5
#IBAction func doForwardJump(_ sender: Any) {
guard let duration = player.currentItem?.duration else{
return
}
let playerCurrentTime = CMTimeGetSeconds(player.currentTime())
let newTime = playerCurrentTime + seekDuration
if newTime < CMTimeGetSeconds(duration) {
let time2: CMTime = CMTimeMake(Int64(newTime * 1000 as Float64), 1000)
player.seek(to: time2)
}
}
#IBAction func doBackwardJump(_ sender: Any) {
let playerCurrentTime = CMTimeGetSeconds(player.currentTime())
var newTime = playerCurrentTime - seekDuration
if newTime < 0 {
newTime = 0
}
let time2: CMTime = CMTimeMake(Int64(newTime * 1000 as Float64), 1000)
player.seek(to: time2)
}
In Objective-C,
#define seekDuration (float)5
- (IBAction)backwardButtonAction:(UIButton *)sender {
float playerCurrentTime = [self getCurrentTime];
float newTime = playerCurrentTime - seekDuration;
if (newTime < 0) {
newTime = 0;
}
CMTime time = CMTimeMake(newTime*1000, 1000);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
playerSliderisScrubbing = NO;
});
}];
}
- (IBAction)forwardButtonAction:(UIButton *)sender {
float duration = [self getPlayerDuration];
float playerCurrentTime = [self getCurrentTime];
float newTime = playerCurrentTime + seekDuration;
if (newTime < duration) {
CMTime time = CMTimeMake(newTime*1000, 1000);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
playerSliderisScrubbing = NO;
});
}];
}
}
- (float)getCurrentTime {
float seconds = 0;
if (_player) {
seconds = CMTimeGetSeconds([_player currentTime]);
}
return seconds;
}
- (float)getPlayerDuration {
float seconds = 0;
if (_player) {
seconds = CMTimeGetSeconds([[_player currentItem] duration]);
}
return seconds;
}

Swift 4, 4.2 & 5
var player : AVPlayer!
#IBAction func fastForwardBtn(_ sender: UIButton) {
let moveForword : Float64 = 5
if player == nil { return }
if let duration = player!.currentItem?.duration {
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
let newTime = playerCurrentTime + moveForword
if newTime < CMTimeGetSeconds(duration)
{
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player!.seek(to: selectedTime)
}
player?.pause()
player?.play()
}
}
#IBAction func rewindBtn(_ sender: UIButton) {
let moveBackword: Float64 = 5
if player == nil
{
return
}
let playerCurrenTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrenTime - moveBackword
if newTime < 0
{
newTime = 0
}
player?.pause()
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player?.seek(to: selectedTime)
player?.play()
}

Use AVPlayer method seekToTime
AVPlayer *player=..;
[player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
here is a reference
Hope it helps

You can use avPlayer.rate to fast backward/forward
Rates: 1.0 normal
0.0 pause
But first, you should check if avPlayerItem can do one of your action
For more info: https://developer.apple.com/LIBRARY/ios/documentation/AVFoundation/Reference/AVPlayer_Class/index.html#//apple_ref/occ/instp/AVPlayer/rate

Swift 5 function to seek forwards or backwards
private let seekDuration: Float64 = 15
private func seekPlayback(isForward: Bool) {
guard
let player = player,
let duration = player.currentItem?.duration
else {
return
}
let currentElapsedTime = player.currentTime().seconds
var destinationTime = isForward ? (currentElapsedTime + seekDuration) : (currentElapsedTime - seekDuration)
if destinationTime < 0 {
destinationTime = 0
}
if destinationTime < duration.seconds {
let newTime = CMTime(value: Int64(destinationTime * 1000 as Float64), timescale: 1000)
player.seek(to: newTime)
}
}

Related

AVAssetWriterInput - add image with a specific duration

I am using these piece of code to create a timelapse movie with a collection of images.
https://github.com/acj/TimeLapseBuilder-Swift/blob/master/TimeLapseBuilder/TimeLapseBuilder.swift
So now I want to add a specific duration between the images. For example... every image should be showed for 3 seconds but the fps of the video should be still 30 fps.
Can somebody help me with this?
videoWriterInput.requestMediaDataWhenReady(on: media_queue) {
let fps: Int32 = 30
let frameDuration = CMTimeMake(1, fps)
let currentProgress = Progress(totalUnitCount: Int64(self.photoURLs.count))
var frameCount: Int64 = 0
var remainingPhotoURLs = [String](self.photoURLs)
while videoWriterInput.isReadyForMoreMediaData && !remainingPhotoURLs.isEmpty {
let nextPhotoURL = remainingPhotoURLs.remove(at: 0)
let lastFrameTime = CMTimeMake(frameCount, fps)
let presentationTime = frameCount == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration)
if !self.appendPixelBufferForImageAtURL(nextPhotoURL, pixelBufferAdaptor: pixelBufferAdaptor, presentationTime: presentationTime) {
error = NSError(
domain: kErrorDomain,
code: kFailedToAppendPixelBufferError,
userInfo: ["description": "AVAssetWriterInputPixelBufferAdapter failed to append pixel buffer"]
)
break
}
frameCount += 1
currentProgress.completedUnitCount = frameCount
progress(currentProgress)
}
videoWriterInput.markAsFinished()
videoWriter.finishWriting {
if let error = error {
failure(error)
} else {
success(videoOutputURL)
}
self.videoWriter = nil
}
}
} else {
error = NSError(
domain: kErrorDomain,
code: kFailedToStartAssetWriterError,
userInfo: ["description": "AVAssetWriter failed to start writing"]
)
}
Many thanks for your help!

AudioQueueStart But no voice

import Foundation
import AudioToolbox
class AudioPlay {
//setting buffer num
static let knumberBuffers = 3
var aqData = AQPlayerState.init()
//A custom structure for a playback audio queue
class AQPlayerState {
var mDataFormat = AudioStreamBasicDescription()
var mQueue:AudioQueueRef?
var mBuffers = [AudioQueueBufferRef?].init(repeating: nil, count: AudioPlay.knumberBuffers)
var mAudioFile:AudioFileID?
var bufferByteSize = UInt32()
var mCurrentPacket:Int64?
var mNumPacketsToRead = UInt32()
var mPacketDescs:UnsafeMutablePointer<AudioStreamPacketDescription>?
var mIsRunning = false
}
//playbackAudioQueue callback
static let HandleOutputBuffer:AudioQueueOutputCallback = { (aqData1, inAQ, inBuffer) in
var pAqData = (aqData1?.assumingMemoryBound(to: AQPlayerState.self).pointee)!
guard pAqData.mIsRunning || pAqData.mQueue != nil else{
print("audioplay is not running exit callback func")
return
}
var numBytesReadFromFile = UInt32()
var numPackets = pAqData.mNumPacketsToRead
AudioFileReadPacketData(pAqData.mAudioFile!, false, &numBytesReadFromFile, pAqData.mPacketDescs, pAqData.mCurrentPacket!, &numPackets, inBuffer.pointee.mAudioData)
if numPackets > 0 {
inBuffer.pointee.mAudioDataByteSize = numBytesReadFromFile
AudioQueueEnqueueBuffer(pAqData.mQueue!, inBuffer, ((pAqData.mPacketDescs != nil) ? numPackets : UInt32(0)), pAqData.mPacketDescs)
pAqData.mCurrentPacket! += Int64(numPackets)
}else{
AudioQueueStop(pAqData.mQueue!, false)
pAqData.mIsRunning = false
}
}
//call func to set the property
//create new outputqueue
//start th audioqueue
func start() {
let url = Bundle.main.url(forResource: "123", withExtension: "mp3")!
let audioFileURL = url as CFURL
print(audioFileURL)
let result = AudioFileOpenURL(audioFileURL, .readPermission, 0, &aqData.mAudioFile)
print(result)
var dataFormatSize = UInt32(MemoryLayout.size(ofValue: aqData.mDataFormat))
let result1 = AudioFileGetProperty(aqData.mAudioFile!, kAudioFilePropertyDataFormat,&dataFormatSize, &aqData.mDataFormat)
//get file property
var maxPacketSize = UInt32()
var propertySize = UInt32(MemoryLayout.size(ofValue: maxPacketSize))
let result2 = AudioFileGetProperty(aqData.mAudioFile!, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize)
//calculate and setting buffer size
DeriveBufferSize(ASBDesc: aqData.mDataFormat, maxPacketSize: maxPacketSize, seconds: 0.5, outBufferSize: &aqData.bufferByteSize, outNumPacketsToRead: &aqData.mNumPacketsToRead)
//check the format is VBR or CBR
let isFormatVBR = aqData.mDataFormat.mBytesPerPacket == 0 || aqData.mDataFormat.mFramesPerPacket == 0
if isFormatVBR {
aqData.mPacketDescs = UnsafeMutablePointer<AudioStreamPacketDescription>.allocate(capacity: MemoryLayout.size(ofValue: AudioStreamPacketDescription()))
}else{
aqData.mPacketDescs = nil
}
//create new audio queue
let result4 = AudioQueueNewOutput(&aqData.mDataFormat,AudioPlay.HandleOutputBuffer, &aqData,CFRunLoopGetCurrent(),CFRunLoopMode.commonModes.rawValue, 0, &aqData.mQueue)
//queue start
aqData.mIsRunning = true
//alloc memory buffer
aqData.mCurrentPacket = 0
for i in 0..<AudioPlay.knumberBuffers {
AudioQueueAllocateBuffer(aqData.mQueue!, aqData.bufferByteSize,&aqData.mBuffers[i])
AudioPlay.HandleOutputBuffer(&aqData,aqData.mQueue!, (aqData.mBuffers[i])!)
}
//start audioqueue
AudioQueueStart(aqData.mQueue!, nil)
repeat{
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 0.25, false)
}while (aqData.mIsRunning)
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 1, false)
}
//calculate and setting buffer size
func DeriveBufferSize(ASBDesc:AudioStreamBasicDescription,maxPacketSize:UInt32,seconds:Float64,outBufferSize:UnsafeMutablePointer<UInt32>,outNumPacketsToRead:UnsafeMutablePointer<UInt32>) {
let maxBufferSize:UInt32 = 0x50000
let minBufferSIze:UInt32 = 0x4000
if ASBDesc.mFramesPerPacket != 0 {
let numPacketsForTime = ASBDesc.mSampleRate / Float64(ASBDesc.mFramesPerPacket) * seconds
outBufferSize.pointee = UInt32(numPacketsForTime) * maxPacketSize
}else{
outBufferSize.pointee = (maxBufferSize > maxPacketSize) ? maxBufferSize:maxPacketSize
}
if outBufferSize.pointee > maxBufferSize && outBufferSize.pointee > maxPacketSize {
outBufferSize.pointee = maxBufferSize
}else{
if outBufferSize.pointee < minBufferSIze{
outBufferSize.pointee = minBufferSIze
}
}
outNumPacketsToRead.pointee = outBufferSize.pointee/maxPacketSize
}
//dispose the audioqueue
func Dispose() {
AudioQueueDispose(aqData.mQueue!, true)
AudioFileClose(aqData.mAudioFile!)
free(aqData.mPacketDescs)
}
}
above code is writed according AudioQueueServiceProgrammingGuide!
create an instence of this class,and call the start() func
compile ok,but no output voice.
I had check the code many times but no advancing
can any one who familiar with audiiqueue help me?
Any help will be appreciated.
replace "AudioFileReadPacketData" with "AudioFileReadPackets" can fix this problem!
but I get a new problem like below sometime! sometimes it works well!
Stream Audio(20535,0x1085ac3c0) malloc: * error for object 0x6080001f6300: Invalid pointer dequeued from free list
* set a breakpoint in malloc_error_break to

How to animate incrementing number in UILabel

I have a label showing a number and I want to change it to a higher number, however I'd like to add a bit of flare to it.
I'd like to have the number increment up to the higher number with an ease inout curve so it speeds up then slows down.
This answer shows how to make it increment (the 2nd answer, not the accepted answer) but I'd rather animate it so I could also make it increase in size slightly then shrink again as well as the ease inout curve.
how to do a running score animation in iphone sdk
Any ideas how best to achieve this?
Thanks
The start/end numbers will be user inputted and I want it to increment up the the end number in the same amount of time. So if I have start 10 end 100 or start 10 end 1000 I want it to count up to the end number in say 5 seconds.
I actually made such a class just for this called UICountingLabel:
http://github.com/dataxpress/UICountingLabel
It allows you to specify whether you want the counting mode to be linear, ease in, ease out, or ease in/out. Ease in/out starts counting slowly, speeds up, and then finishes slowly - all in whatever amount of time you specify.
It doesn't currently support setting the actual font size of the label based on the current value, though I may add support for that if it's a feature that's in-demand. Most of the labels in my layouts don't have a lot of room to grow or shrink, so I'm not sure how you want to use it. However, it behaves totally like a normal label, so you can change the font size on your own as well.
Here #malex's answer in swift 3.
func incrementLabel(to endValue: Int) {
let duration: Double = 2.0 //seconds
DispatchQueue.global().async {
for i in 0 ..< (endValue + 1) {
let sleepTime = UInt32(duration/Double(endValue) * 1000000.0)
usleep(sleepTime)
DispatchQueue.main.async {
self.myLabel.text = "\(i)"
}
}
}
}
However, I strongly recommend simply downloading this class from GitHub and dragging it into your project, I've resorted to using it because the timing in my code doesn't seem to adjust properly for lower/higher count numbers. This class works great and looks very good. See this medium article for reference.
You can use GCD to shift delays to background threads.
Here is the example of value animation (from 1 to 100 in 10 seconds)
float animationPeriod = 10;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
for (int i = 1; i < 101; i ++) {
usleep(animationPeriod/100 * 1000000); // sleep in microseconds
dispatch_async(dispatch_get_main_queue(), ^{
yourLabel.text = [NSString stringWithFormat:#"%d", i];
});
}
});
You could use a flag to see if it has to go up or down.
Instead of a for loop, use a while loop.
In this way, you are creating a loop that keeps going, so you have to find a way to stop it also, f.e. by a button press.
There you have it without blocking sleeps!
stick this to a UILabel and it counts up and down from the value currently displaying, cleaning non numbers first.
Adjust from Double to Int or Float if needed.
yourlabel.countAnimation(upto: 100.0)
another simple alternative
extension UILabel {
func countAnimation(upto: Double) {
let from: Double = text?.replace(string: ",", replacement: ".").components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted).first.flatMap { Double($0) } ?? 0.0
let steps: Int = 20
let duration = 0.350
let rate = duration / Double(steps)
let diff = upto - from
for i in 0...steps {
DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(i)) {
self.text = "\(from + diff * (Double(i) / Double(steps)))"
}
}
}
}
Swift 4 Code :
let animationPeriod: Float = 1
DispatchQueue.global(qos: .default).async(execute: {
for i in 1..<Int(endValue) {
usleep(useconds_t(animationPeriod / 10 * 10000)) // sleep in microseconds
DispatchQueue.main.async(execute: {
self.lbl.text = "\(i+1)"
})
}
})
If you want a fast counting animation, you can use a recursive func like so:
func updateGems(diff: Int) {
animateIncrement(diff: diff, index: 0, start: 50)
}
func animateIncrement(diff: Int, index: Int, start: Int) {
if index == diff {return}
gemsLabel.text = "\(start + index)"
DispatchQueue.main.asyncAfter(deadline: .now() + 0.002) {
self.animateIncrement(diff: diff, index: index + 1, start: start)
}
}
For the ones looking for a linear slow down counter (Swift 5) :
func animateCountLabel(to userCount: Int) {
countLabel.text = " "
let countStart: Int = userCount - 10
let countEnd: Int = userCount
DispatchQueue.global(qos: .default).async { [weak self] in
for i in (countStart...countEnd) {
let delayTime = UInt64(pow(2, (Float(i) - Float(countStart))))
usleep(useconds_t( delayTime * 1300 )) // sleep in microseconds
DispatchQueue.main.async { [weak self] in
self?.countLabel.text = "\(i)"
}
}
}
}
You can change the start end points and slow down speed by changing static values to your needs ;)
My variation of #Sergio's answer:
extension UILabel {
func countAnimation(upto: Double) {
let fromString = text?.replacingOccurrences(of: "Score: ", with: "")
let from: Double = fromString?.replacingOccurrences(of: ",", with: ".")
.components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted)
.first.flatMap { Double($0) } ?? 0.0
let duration = 0.4
let diff = upto - from
let rate = duration / diff
for item in 0...Int(diff) {
DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(item)) {
self.text = "Score: \(Int(from + diff * (Double(item) / diff)))"
}
}
}
}
So amount of steps is based on the difference between starting and ending values
This is how i did it :
- (void)setupAndStartCounter:(CGFloat)duration {
NSUInteger step = 3;//use your own logic here to define the step.
NSUInteger remainder = numberYouWantToCount%step;//for me it was 30
if (step < remainder) {
remainder = remainder%step;
}
self.aTimer = [self getTimer:[self getInvocation:#selector(animateLabel:increment:) arguments:[NSMutableArray arrayWithObjects:[NSNumber numberWithInteger:remainder], [NSNumber numberWithInteger:step], nil]] timeInterval:(duration * (step/(float) numberYouWantToCountTo)) willRepeat:YES];
[self addTimerToRunLoop:self.aTimer];
}
- (void)animateLabel:(NSNumber*)remainder increment:(NSNumber*)increment {
NSInteger finish = finalValue;
if ([self.aLabel.text integerValue] <= (finish) && ([self.aLabel.text integerValue] + [increment integerValue]<= (finish))) {
self.aLabel.text = [NSString stringWithFormat:#"%lu",(unsigned long)([self.aLabel.text integerValue] + [increment integerValue])];
}else{
self.aLabel.text = [NSString stringWithFormat:#"%lu",(unsigned long)([self.aLabel.text integerValue] + [remainder integerValue])];
[self.aTimer invalidate];
self.aTimer = nil;
}
}
#pragma mark -
#pragma mark Timer related Functions
- (NSTimer*)getTimer:(NSInvocation *)invocation timeInterval:(NSTimeInterval)timeInterval willRepeat:(BOOL)willRepeat
{
return [NSTimer timerWithTimeInterval:timeInterval invocation:invocation repeats:willRepeat];
}
- (NSInvocation*)getInvocation:(SEL)methodName arguments:(NSMutableArray*)arguments
{
NSMethodSignature *sig = [self methodSignatureForSelector:methodName];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:self];
[invocation setSelector:methodName];
if (arguments != nil)
{
id arg1 = [arguments objectAtIndex:0];
id arg2 = [arguments objectAtIndex:1];
[invocation setArgument:&arg1 atIndex:2];
[invocation setArgument:&arg2 atIndex:3];
}
return invocation;
}
- (void)addTimerToRunLoop:(NSTimer*)timer
{
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
You can also check https://github.com/leszek-s/LSCategories
It allows incrementing/decrementing number in UILabel with a single line of code like this:
[self.label lsAnimateCounterWithStartValue:10 endValue:100 duration:5 completionBlock:nil];
Details
Xcode 9.2, swift 4
Solution
class LoadingProcess {
let minValue: Int
let maxValue: Int
var currentValue: Int
private let progressQueue = DispatchQueue(label: "ProgressView")
private let semaphore = DispatchSemaphore(value: 1)
init (minValue: Int, maxValue: Int) {
self.minValue = minValue
self.currentValue = minValue
self.maxValue = maxValue
}
private func delay(stepDelayUsec: useconds_t, completion: #escaping ()->()) {
usleep(stepDelayUsec)
DispatchQueue.main.async {
completion()
}
}
func simulateLoading(toValue: Int, step: Int = 1, stepDelayUsec: useconds_t? = 10_000,
valueChanged: #escaping (_ currentValue: Int)->(),
completion: ((_ currentValue: Int)->())? = nil) {
semaphore.wait()
progressQueue.sync {
if currentValue <= toValue && currentValue <= maxValue {
usleep(stepDelayUsec!)
DispatchQueue.main.async {
valueChanged(self.currentValue)
self.currentValue += step
self.semaphore.signal()
self.simulateLoading(toValue: toValue, step: step, stepDelayUsec: stepDelayUsec, valueChanged: valueChanged, completion: completion)
}
} else {
self.semaphore.signal()
completion?(currentValue)
}
}
}
func finish(step: Int = 1, stepDelayUsec: useconds_t? = 10_000,
valueChanged: #escaping (_ currentValue: Int)->(),
completion: ((_ currentValue: Int)->())? = nil) {
simulateLoading(toValue: maxValue, step: step, stepDelayUsec: stepDelayUsec, valueChanged: valueChanged, completion: completion)
}
}
Usage
let loadingProcess = LoadingProcess(minValue: 0, maxValue: 100)
loadingProcess.simulateLoading(toValue: 80, valueChanged: { currentValue in
// Update views
})
DispatchQueue.global(qos: .background).async {
print("Start loading data")
sleep(5)
print("Data loaded")
loadingProcess.finish(valueChanged: { currentValue in
// Update views
}) { _ in
print("End")
}
}
Full sample
Do not forget to add the solution code here
import UIKit
class ViewController: UIViewController {
weak var counterLabel: UILabel!
weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fillProportionally
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 80).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -80).isActive = true
let label = UILabel()
label.textAlignment = .center
label.text = "0"
label.font = UIFont.systemFont(ofSize: 46)
stackView.addArrangedSubview(label)
counterLabel = label
let progressView = UIProgressView()
progressView.trackTintColor = .lightGray
progressView.progressTintColor = .blue
progressView.layer.cornerRadius = 4
progressView.clipsToBounds = true
progressView.heightAnchor.constraint(equalToConstant: 8).isActive = true
stackView.addArrangedSubview(progressView)
self.progressView = progressView
let button = UIButton()
button.setTitle("Start", for: .normal)
button.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside)
button.setTitleColor(.blue, for: .normal)
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
stackView.addArrangedSubview(button)
}
#objc func startButtonTapped() {
sample()
}
private func setProcess(currentValue: Int) {
let value = 0.01 * Float(currentValue)
self.counterLabel?.text = "\(currentValue)"
self.progressView?.setProgress(value, animated: true)
print("\(currentValue)")
}
func sample() {
let loadingProcess = LoadingProcess(minValue: 0, maxValue: 100)
loadingProcess.simulateLoading(toValue: 80, valueChanged: { currentValue in
self.setProcess(currentValue: currentValue)
})
DispatchQueue.global(qos: .background).async {
print("Start loading data")
sleep(5)
print("Data loaded")
loadingProcess.finish(valueChanged: { currentValue in
self.setProcess(currentValue: currentValue)
}) { _ in
print("end")
}
}
}
}
Results

Timeline Progress bar for AVPlayer

AVPlayer is fully customizable, unfortunately there are convenient methods in AVPlayer for showing the time line progress bar.
AVPlayer *player = [AVPlayer playerWithURL:URL];
AVPlayerLayer *playerLayer = [[AVPlayerLayer playerLayerWithPlayer:avPlayer] retain];[self.view.layer addSubLayer:playerLayer];
I have an progress bar that indicates the how video has been played, and how much remained just as like MPMoviePlayer.
So how to get the timeline of video from AVPlayer and how to update the progress bar
Suggest me.
Please use the below code which is from apple example code "AVPlayerDemo".
double interval = .1f;
CMTime playerDuration = [self playerItemDuration]; // return player duration.
if (CMTIME_IS_INVALID(playerDuration))
{
return;
}
double duration = CMTimeGetSeconds(playerDuration);
if (isfinite(duration))
{
CGFloat width = CGRectGetWidth([yourSlider bounds]);
interval = 0.5f * duration / width;
}
/* Update the scrubber during normal playback. */
timeObserver = [[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC)
queue:NULL
usingBlock:
^(CMTime time)
{
[self syncScrubber];
}] retain];
- (CMTime)playerItemDuration
{
AVPlayerItem *thePlayerItem = [player currentItem];
if (thePlayerItem.status == AVPlayerItemStatusReadyToPlay)
{
return([playerItem duration]);
}
return(kCMTimeInvalid);
}
And in syncScrubber method update the UISlider or UIProgressBar value.
- (void)syncScrubber
{
CMTime playerDuration = [self playerItemDuration];
if (CMTIME_IS_INVALID(playerDuration))
{
yourSlider.minimumValue = 0.0;
return;
}
double duration = CMTimeGetSeconds(playerDuration);
if (isfinite(duration) && (duration > 0))
{
float minValue = [ yourSlider minimumValue];
float maxValue = [ yourSlider maximumValue];
double time = CMTimeGetSeconds([player currentTime]);
[yourSlider setValue:(maxValue - minValue) * time / duration + minValue];
}
}
Thanks to iOSPawan for the code!
I simplified the code to the necessary lines. This might be more clear to understand the concept. Basically I have implemented it like this and it works fine.
Before starting the video:
__weak NSObject *weakSelf = self;
[_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC)
queue:NULL
usingBlock:^(CMTime time){
[weakSelf updateProgressBar];
}];
[_player play];
Then you need to have a method to update your progress bar:
- (void)updateProgressBar
{
double duration = CMTimeGetSeconds(_playerItem.duration);
double time = CMTimeGetSeconds(_player.currentTime);
_progressView.progress = (CGFloat) (time / duration);
}
let progressView = UIProgressView(progressViewStyle: UIProgressViewStyle.Bar)
self.view.addSubview(progressView)
progressView.constrainHeight("\(1.0/UIScreen.mainScreen().scale)")
progressView.alignLeading("", trailing: "", toView: self.view)
progressView.alignBottomEdgeWithView(self.view, predicate: "")
player.addPeriodicTimeObserverForInterval(CMTimeMakeWithSeconds(1/30.0, Int32(NSEC_PER_SEC)), queue: nil) { time in
let duration = CMTimeGetSeconds(playerItem.duration)
progressView.progress = Float((CMTimeGetSeconds(time) / duration))
}
I know it's an old question, but someone may find it useful. It's only Swift version:
//set the timer, which will update your progress bar. You can use whatever time interval you want
private func setupProgressTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true, block: { [weak self] (completion) in
guard let self = self else { return }
self.updateProgress()
})
}
//update progression of video, based on it's own data
private func updateProgress() {
guard let duration = player?.currentItem?.duration.seconds,
let currentMoment = player?.currentItem?.currentTime().seconds else { return }
progressBar.progress = Float(currentMoment / duration)
}
Swifty answer to get progress:
private func addPeriodicTimeObserver() {
// Invoke callback every half second
let interval = CMTime(seconds: 0.5,
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
self.playerController?.player?.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) { [weak self] time in
let currentSeconds = CMTimeGetSeconds(time)
guard let duration = self?.playerController?.player?.currentItem?.duration else { return }
let totalSeconds = CMTimeGetSeconds(duration)
let progress: Float = Float(currentSeconds/totalSeconds)
print(progress)
}
}
Ref
In my case, the following code works Swift 3:
var timeObserver: Any?
override func viewDidLoad() {
........
let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = avPlayer.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsedTime in
self.updateSlider(elapsedTime: elapsedTime)
})
}
func updateSlider(elapsedTime: CMTime) {
let playerDuration = playerItemDuration()
if CMTIME_IS_INVALID(playerDuration) {
seekSlider.minimumValue = 0.0
return
}
let duration = Float(CMTimeGetSeconds(playerDuration))
if duration.isFinite && duration > 0 {
seekSlider.minimumValue = 0.0
seekSlider.maximumValue = duration
let time = Float(CMTimeGetSeconds(elapsedTime))
seekSlider.setValue(time, animated: true)
}
}
private func playerItemDuration() -> CMTime {
let thePlayerItem = avPlayer.currentItem
if thePlayerItem?.status == .readyToPlay {
return thePlayerItem!.duration
}
return kCMTimeInvalid
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
avPlayer.removeTimeObserver(timeObserver!)
}
for timeline i do this
-(void)changeSliderValue {
double duration = CMTimeGetSeconds(self.player.currentItem.duration);
[lengthSlider setMaximumValue:(float)duration];
lengthSlider.value = CMTimeGetSeconds([self.player currentTime]);
int seconds = lengthSlider.value,minutes = seconds/60,hours = minutes/60;
int secondsRemain = lengthSlider.maximumValue - seconds,minutesRemain = secondsRemain/60,hoursRemain = minutesRemain/60;
seconds = seconds-minutes*60;
minutes = minutes-hours*60;
secondsRemain = secondsRemain - minutesRemain*60;
minutesRemain = minutesRemain - hoursRemain*60;
NSString *hourStr,*minuteStr,*secondStr,*hourStrRemain,*minuteStrRemain,*secondStrRemain;
hourStr = hours > 9 ? [NSString stringWithFormat:#"%d",hours] : [NSString stringWithFormat:#"0%d",hours];
minuteStr = minutes > 9 ? [NSString stringWithFormat:#"%d",minutes] : [NSString stringWithFormat:#"0%d",minutes];
secondStr = seconds > 9 ? [NSString stringWithFormat:#"%d",seconds] : [NSString stringWithFormat:#"0%d",seconds];
hourStrRemain = hoursRemain > 9 ? [NSString stringWithFormat:#"%d",hoursRemain] : [NSString stringWithFormat:#"0%d",hoursRemain];
minuteStrRemain = minutesRemain > 9 ? [NSString stringWithFormat:#"%d",minutesRemain] : [NSString stringWithFormat:#"0%d",minutesRemain];
secondStrRemain = secondsRemain > 9 ? [NSString stringWithFormat:#"%d",secondsRemain] : [NSString stringWithFormat:#"0%d",secondsRemain];
timePlayed.text = [NSString stringWithFormat:#"%#:%#:%#",hourStr,minuteStr,secondStr];
timeRemain.text = [NSString stringWithFormat:#"-%#:%#:%#",hourStrRemain,minuteStrRemain,secondStrRemain];
And import CoreMedia framework
lengthSlider is UISlider
I took the answers from the iOSPawan and Raphael and then adapted to my needs.
So I have music and UIProgressView which is always in loop and when you go to the next screen and come back the the song and the bar continued where they were left.
Code:
#interface YourClassViewController (){
NSObject * periodicPlayerTimeObserverHandle;
}
#property (nonatomic, strong) AVPlayer *player;
#property (nonatomic, strong) UIProgressView *progressView;
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if(_player != nil && ![self isPlaying])
{
[self musicPlay];
}
}
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (_player != nil) {
[self stopPlaying];
}
}
// ----------
// PLAYER
// ----------
-(BOOL) isPlaying
{
return ([_player rate] > 0);
}
-(void) musicPlay
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[_player currentItem]];
__weak typeof(self) weakSelf = self;
periodicPlayerTimeObserverHandle = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC)
queue:NULL
usingBlock:^(CMTime time){
[weakSelf updateProgressBar];
}];
[_player play];
}
-(void) stopPlaying
{
#try {
if(periodicPlayerTimeObserverHandle != nil)
{
[_player removeTimeObserver:periodicPlayerTimeObserverHandle];
periodicPlayerTimeObserverHandle = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
[_player pause];
}
#catch (NSException * __unused exception) {}
}
-(void) playPreviewSong:(NSURL *) previewSongURL
{
[self configureAVPlayerAndPlay:previewSongURL];
}
-(void) configureAVPlayerAndPlay: (NSURL*) url {
if(_player)
[self stopPlaying];
AVAsset *audioFileAsset = [AVURLAsset URLAssetWithURL:url options:nil];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:audioFileAsset];
_player = [AVPlayer playerWithPlayerItem:playerItem];
[_player addObserver:self forKeyPath:#"status" options:0 context:nil];
CRLPerformBlockOnMainThreadAfterDelay(^{
NSError *loadErr;
if([audioFileAsset statusOfValueForKey:#"playable" error:&loadErr] == AVKeyValueStatusLoading)
{
[audioFileAsset cancelLoading];
[self stopPlaying];
[self showNetworkError:NSLocalizedString(#"Could not play file",nil)];
}
}, NETWORK_REQUEST_TIMEOUT);
}
- (void)updateProgressBar
{
double currentTime = CMTimeGetSeconds(_player.currentTime);
if(currentTime <= 0.05){
[_progressView setProgress:(float)(0.0) animated:NO];
return;
}
if (isfinite(currentTime) && (currentTime > 0))
{
float maxValue = CMTimeGetSeconds(_player.currentItem.asset.duration);
[_progressView setProgress:(float)(currentTime/maxValue) animated:YES];
}
}
-(void) showNetworkError:(NSString*)errorMessage
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(#"No connection", nil) message:errorMessage preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(#"OK", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
// do nothing
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == _player && [keyPath isEqualToString:#"status"]) {
if (_player.status == AVPlayerStatusFailed) {
[self showNetworkError:NSLocalizedString(#"Could not play file", nil)];
} else if (_player.status == AVPlayerStatusReadyToPlay) {
NSLog(#"AVPlayerStatusReadyToPlay");
[TLAppAudioAccess setAudioAccess:TLAppAudioAccessType_Playback];
[self musicPlay];
} else if (_player.status == AVPlayerItemStatusUnknown) {
NSLog(#"AVPlayerItemStatusUnknown");
}
}
}
- (void)playerItemDidReachEnd:(NSNotification *)notification {
if ([notification.object isEqual:self.player.currentItem])
{
[self.player seekToTime:kCMTimeZero];
[self.player play];
}
}
-(void) dealloc{
#try {
[_player removeObserver:self forKeyPath:#"status"];
}
#catch (NSException * __unused exception) {}
[self stopPlaying];
_player = nil;
}
technically you don't need a timer for this one.
Just add property
private var isUserDragingSlider: Bool = false
Then you need to set up a slider target.
STEP 1
self.timeSlider.addTarget(self, action: #selector(handleSliderChangeValue(slider:event:)), for: .allEvents)
and in func handleSliderChangeValue you need to add this:
STEP 2
#objc
func handleSliderChangeValue(slider: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
self.isUserDragingSlider = true
case .ended:
self.isUserDragingSlider = false
self.updateplayerWithSliderChangeValue()
default:
break
}
}
}
and in the end, you need to update your player with the selected time from a slider.
STEP 3
func updateplayerWithSliderChangeValue() {
if let duration = player?.currentItem?.duration {
let totalSeconds = CMTimeGetSeconds(duration)
if !(totalSeconds.isNaN || totalSeconds.isInfinite) {
let newCurrentTime: TimeInterval = Double(self.timeSlider.value) * CMTimeGetSeconds(duration)
let seekToTime: CMTime = CMTimeMakeWithSeconds(newCurrentTime, preferredTimescale: 600)
self.player?.seek(to: seekToTime)
self.isUserDragingSlider.toggle()
}
}
}
And last thing, You need to update your code. Video is updating your slider.
STEP 4
func setupSliderValue(_ seconds: Float64) {
guard !(seconds.isNaN || seconds.isInfinite) else {
return
}
if !isUserDragingSlider {
if let duration = self.player?.currentItem?.duration {
let durationInSeconds = CMTimeGetSeconds(duration)
self.timeSlider.value = Float(seconds / durationInSeconds)
}
}
}
So main problem here is that when we move the slider we have a conflict between updating the slider ( from video ) and updating the video with our slider change.
That is why the slider is not working well. When you block updates from video time to slider with isUserDragingSlider all is working fine.

UISlider to control AVAudioPlayer

I'm trying to implement a little function in my app. I am currently playing sounds as AVAudioPlayers and that works fine. What I would like to add is to control the sound's position (currentTime) with an UISlider: is there a simple way to do it ?
I looked at an Apple project but it was quite messy....have you got samples or suggestions ?
Thanks to everyone in advance
Shouldn't be a problem - just set the slider to continuous and set the max value to your player's duration after loading your sound file.
Edit
I just did this and it works for me...
- (IBAction)slide {
player.currentTime = slider.value;
}
- (void)updateTime:(NSTimer *)timer {
slider.value = player.currentTime;
}
- (IBAction)play:(id)sender {
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"sound.caf" ofType:nil]];
NSError *error;
player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if (!player) NSLog(#"Error: %#", error);
[player prepareToPlay];
slider.maximumValue = [player duration];
slider.value = 0.0;
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(updateTime:) userInfo:nil repeats:YES];
[player play];
}
The slider is configured in IB, as is a button to start playing.
Swift 3.0 Update:
var player: AVAudioPlayer!
var sliderr: UISlider!
#IBAction func play(_ sender: Any) {
var url = URL(fileURLWithPath: Bundle.main.path(forResource: "sound.caf", ofType: nil)!)
var error: Error?
do {
player = try AVAudioPlayer(contentsOf: url)
}
catch let error {
}
if player == nil {
print("Error: \(error)")
}
player.prepareToPlay()
sliderr.maximumValue = Float(player.duration)
sliderr.value = 0.0
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
player.play()
}
func updateTime(_ timer: Timer) {
sliderr.value = Float(player.currentTime)
}
#IBAction func slide(_ slider: UISlider) {
player.currentTime = TimeInterval(slider.value)
}
To extend on paull's answer, you'd set the slider to be continuous with a maximum value of your audio player's duration, then add some object of yours (probably the view controller) as a target for the slider's UIControlEventValueChanged event; when you receive the action message, you'd then set the AVAudioPlayer's currentTime property to the slider's value.
You might also want to use an NSTimer to update the slider's value as the audio player plays; +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: is the easiest way to do that.
I needed to adapt the above answer a bit to get it to work. The issue is that using
slider.maximumValue = [player duration];
slider.value = player.currentTime;
player.currentTime = slider.value;
Do not work because the slider expects a float and the player currentTime and dration return CMTime. To make these work, I adapted them to read:
slider.maximumValue = CMTimeGetSeconds([player duration]);
slider.value = CMTimeGetSeconds(player.currentTime);
player.currentTime = CMTimeMakeWithSeconds((int)slider.value,1);
If you don't need any data in between drag, then you should simply set:
mySlider.isContinuous = false
Otherwise, try below code to controller each phase of touch.
// audio slider bar
private lazy var slider: UISlider = {
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumTrackTintColor = .red
slider.maximumTrackTintColor = .white
slider.setThumbImage(UIImage(named: "sliderThumb"), for: .normal)
slider.addTarget(self, action: #selector(onSliderValChanged(slider:event:)), for: .valueChanged)
// slider.isContinuous = false
return slider
}()
#objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
guard let player = AudioPlayer.shared.player else { return }
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
// handle drag began
// I would stop the timer when drag begin
timer.invalidate()
case .moved:
// handle drag moved
// Update label's text for current playing time
case .ended:
// update the player's currTime and re-create the timer when drag is done.
player.currentTime = TimeInterval(slider.value)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTime(_:)), userInfo: nil, repeats: true)
default:
break
}
}
}
Problems that I've faced during playing an audio file and show start/end time and controlling the song with the UISlider.
Not playing audio directly without downloading it in temp folder.
UISlider got crashed on main thread in lower iOS version i.e 12.4/13.1
Smooth Scrolling of UISlider.
Calculating and updating the start/end time of the song.
This answer needs some editing, but it will work without any doubt.
//UISlider init
lazy var slider: UISlider = {
let progress = UISlider()
progress.minimumValue = 0.0
progress.maximumValue = 100.0
progress.tintColor = UIColor.init(named: "ApplicationColor")
return progress
}()
var audioPlayer : AVAudioPlayer?
//First I've downloaded the audio and then playing it.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(trackAudio), userInfo: nil, repeats: true)
if let audioURLString = audioURL{
let urlstring = URL(string: audioURLString)!
downloadFromURL(url: urlstring) { (localURL, response, error) in
if let localURL = localURL{
self.playAudioFile(url: localURL)
}
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopTimer()
}
// Stop TimeInterval After View disappear
func stopTimer() {
if timer != nil {
timer?.invalidate()
audioPlayer?.stop()
audioPlayer = nil
timer = nil
}
}
#objc func sliderSelected(_ sender : UISlider){
if audioPlayer != nil{
if !isPlaying{
self.audioPlayer?.play()
playButton.setImage(UIImage.init(named: "AudioPause"), for: .normal)
isPlaying = true
}else{
self.audioPlayer?.currentTime = TimeInterval(Float(sender.value) * Float(self.audioPlayer!.duration) / 100.0)
if (sender.value / 100.0 == 1.0){
//Do something if audio ends while dragging the UISlider.
}
}
}
}
func downloadFromURL(url:URL,completion: #escaping((_ downladedURL: URL?,_ response :URLResponse?,_ error: Error?) -> Void)){
var downloadTask:URLSessionDownloadTask
downloadTask = URLSession.shared.downloadTask(with: url) {(URL, response, error) in
if let url = URL{
completion(url,nil,nil)
}else if let response = response{
completion(nil,response,nil)
}
if let error = error{
completion(nil,nil,error)
}
}
downloadTask.resume()
}
func playAudioFile(url:URL){
do{
self.audioPlayer = try AVAudioPlayer(contentsOf: url)
self.audioPlayer?.prepareToPlay()
self.audioPlayer?.delegate = self
self.audioPlayer?.play()
let audioDuration = audioPlayer?.duration
let audioDurationSeconds = audioDuration
minutes = Int(audioDurationSeconds!/60);
seconds = Int(audioDurationSeconds!.truncatingRemainder(dividingBy: 60))
} catch{
print("AVAudioPlayer init failed")
}
}
#objc func trackAudio() {
if audioPlayer != nil{
DispatchQueue.main.async {
print("HI")
let normalizedTime = Float(self.audioPlayer!.currentTime * 100.0 / self.audioPlayer!.duration)
self.slider.setValue(normalizedTime, animated: true)
let currentTime = self.audioPlayer?.currentTime
self.currentMinutes = Int(currentTime!/60);
self.currentSeconds = Int(currentTime!.truncatingRemainder(dividingBy: 60))
self.startTimeLabel.text = String(format: "%02i:%02i", self.currentMinutes, self.currentSeconds)
self.endTimeLabel.text = String(format: "%02i:%02i", self.minutes, self.seconds)
}
}
}
If anyone was looking for a simple TouchDown and TouchUp on UI slider then this turns out to be as simple as :
slider.addTarget(self, action: #selector(changeVlaue(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTapped), for: .touchDown)
slider.addTarget(self, action: #selector(sliderUntouched), for: .touchUpInside)
Here's the entire setup for an AVAudioPlayer. Some of the code in handleScrubbing() and fingerLiftedFromSlider() is duplicated but whatever...
This will let you show what's on the currentTimeLabel (usually on the left) and the totalDurationLabel (usually on the right) with the scrubber/slider in the the middle of them. When you slide the slider the currentTime will update to show wherever the slider is.
There is something to be aware about. If the the player was playing before you touch the slider, while you slide the slider, the player is still playing. In .began you need to check if the player was playing and if so pause it and set a variable like wasAudioPlayerPlayingBeforeSliderWasTouched to true so that when your finger is lifted it will continue playing from wherever you lift your finger. If you don't pause the player then the slider isn't going to slide smoothly.
When you lift your finger there is a check in onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() to see if the slider is at its endTime. If it is instead of playing it'll run the code in audioEndTimeStopEverything().
In the startAudioPlayer method, I used an AVURLAsset get a set the actual url's duration. I got it from this answer which has a great explanation.
I used this code with a local url, not sure how this will work with a remote url.
import UIKit
import AVFoundation
class MyAudioController: UIViewController {
lazy var currentTimeLabel ... { ... }()
lazy var totalDurationLabel ... { ... }()
lazy vay pausePlayButton ... { ... }()
lazy var fastForwardButton ... { ... }()
lazy var rewindButton ... { ... }()
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.isContinuous = true
s.minimumTrackTintColor = UIColor.red
s.maximumTrackTintColor = UIColor.blue
s.setThumbImage(UIImage(named: "circleIcon"), for: .normal)
s.addTarget(self, action: #selector(sliderValChanged(slider:event:)), for: .valueChanged)
return s
}()
weak var timer: Timer? // *** MAKE SURE THIS IS WEAK ***
var audioPlayer: AVAudioPlayer?
var wasAudioPlayerPlayingBeforeSliderWasTouched = false
override func viewDidLoad() {
super.viewDidLoad()
guard let myAudioUrl = URL(string: "...") else { return }
setAudio(with: myAudioUrl)
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print(error)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopAudioPlayer()
}
// 1. init your AVAudioPlayer here
func setAudioPlayer(with audioTrack: URL) {
do {
stopAudioPlayer() // if something was previously playing
audioPlayer = try AVAudioPlayer(contentsOf: audioTrack)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
audioPlayer?.volume = audioVolume
startAudioPlayer()
} catch let err as NSError {
print(err.localizedDescription)
}
}
// 2. Audio PLAYER - start / stop funcs
stopAudioPlayer() {
stopAudioTimer()
audioPlayer?.pause()
audioPlayer?.stop()
}
func startAudioPlayer() {
if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
audioPlayer.pause()
}
audioPlayer?.currentTime = 0
audioPlayer?.play()
pausePlayButton.setImage(UIImage(named: "pauseIcon"), for: .normal)
startAudioTimer()
}
func startAudioTimer() {
stopAudioTimer()
slider.value = 0
currentTimeLabel.text = "00:00"
totalDurationLabel.text = "00:00"
guard let url = audioPlayer?.url else { return }
let assetOpts = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
let asset = AVURLAsset(url: url, options: assetOpts)
let assetDuration: CMTime = asset.duration
let assetDurationInSecs: Float64 = CMTimeGetSeconds(assetDuration)
slider.maximumValue = Float(assetDurationInSecs)
totalDurationLabel.text = strFromTimeInterval(interval: TimeInterval(assetDurationInSecs))
runAudioTimer()
}
// 3. TIMER funcs
func runAudioTimer() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { [weak self](_) in
self?.audioTimerIsRunning()
})
}
}
func audioTimerIsRunning() {
guard let audioPlayer = audioPlayer else { return }
let currentTime = audioPlayer.currentTime
if Float(currentTime) >= Float(slider.maximumValue) {
stopAudioTimer()
}
currentTimeLabel.text = strFromTimeInterval(interval: currentTime)
slider.value = Float(currentTime)
}
func stopAudioTimer() {
if timer != nil {
timer?.invalidate()
timer = nil
}
}
// slider funcs
#objc func sliderValChanged(slider: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched()
stopAudioTimer()
print("Finger Touched")
case .moved:
handleScrubbing()
print("Finger is Moving Scrubber")
case .ended:
print("Finger Lifted")
onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan()
fingerLiftedFromSlider()
default:
print("Something Else Happened In Slider")
}
}
}
func checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched() {
guard let audioPlayer = audioPlayer else { return }
if audioPlayer.isPlaying {
audioPlayer.pause()
wasAudioPlayerPlayingBeforeSliderWasTouched = true
}
}
func handleScrubbing() {
guard let audioPlayer = audioPlayer else { return }
let sliderValue = TimeInterval(slider.value)
currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)
audioPlayer.currentTime = sliderValue
if audioPlayer.currentTime >= audioPlayer.duration {
audioEndTimeStopEverything()
}
}
func onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() {
if wasAudioPlayerPlayingBeforeSliderWasTouched {
wasAudioPlayerPlayingBeforeSliderWasTouched = false
guard let audioPlayer = audioPlayer else { return }
if slider.value >= slider.maximumValue {
audioEndTimeStopEverything()
} else {
audioPlayer.play()
}
}
}
func fingerLiftedFromSlider() {
guard let audioPlayer = audioPlayer else { return }
if !audioPlayer.isPlaying { // this check is necessary because if you paused the audioPlayer, then started sliding, it should still be paused when you lift you finger up. It it's paused there is no need for the timer function to run.
let sliderValue = TimeInterval(slider.value)
currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)
audioPlayer.currentTime = sliderValue
return
}
runAudioTimer()
}
func audioEndTimeStopEverything() {
stopAudioPlayer()
pausePlayButton.setImage(UIImage("named: playIcon"), for: .normal)
guard let audioPlayer = audioPlayer else { return }
// for some reason when the audioPlayer would reach its end time it kept resetting its currentTime property to zero. I don't know if that's meant to happen or a bug but the currentTime would be zero and the slider would be at the end. To rectify the issue I set them both to their end times
audioPlayer.currentTime = audioPlayer.duration
slider.value = slider.maximumValue
}
}
extension MyAudioController: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
audioEndTimeStopEverything()
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
if let error = error {
print(error.localizedDescription)
}
}
}
Here is the strFromTimeInterval(interval: ) function that I got from here. I only used it because I didn't want to bother with milliseconds. The code above was ran using audio files with minutes and seconds, not hours. If you have any problems with hours you can also swap this function out for this answer
extension MyAudioController {
func strFromTimeInterval(interval: TimeInterval) -> String {
let time = NSInteger(interval)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
var formatString = ""
if hours == 0 {
if (minutes < 10) {
formatString = "%2d:%0.2d"
} else {
formatString = "%0.2d:%0.2d"
}
return String(format: formatString,minutes,seconds)
} else {
formatString = "%2d:%0.2d:%0.2d"
return String(format: formatString,hours,minutes,seconds)
}
}
}