How to design a custom AVPlayer to play audio using URL in iOS swift.

Santosh Kumar J M
4 min readJul 29, 2020

--

What is AVPlayer?
An AVPlayer is a controller object used to play movies and mp3 files that are stored locally or using URLs.

Introduction:-

Custom AVPlayer

Let’s create custom AVPlayer controls as shown in the fig above:-
1. custom slider
2. play and pause Button
3. seek forward or backward
4. Display current time and overAll duration.

Let’s start creating the UI design in Main.storyboard and do the setup of the views.

List of components we have added:-
1. Progress view
2. three buttons for play, seek forward and seek backward
3. two label to show current time and over all duration.

First we will import AVFoundation

import UIKit
import AVFoundation

then create AVPlayer and AVPlayerItem and define seek duration in float and then create outlets:-

var player:AVPlayer?
var playerItem:AVPlayerItem?
fileprivate let seekDuration: Float64 = 10

@IBOutlet weak var labelOverallDuration: UILabel!
@IBOutlet weak var labelCurrentTime: UILabel!
@IBOutlet weak var playbackSlider: UISlider!
@IBOutlet weak var loadingView: UIActivityIndicatorView!
@IBOutlet weak var ButtonPlay: UIButton!

call the below code in viewDidLoad

let url = URL(string: “AUDIO_URL_HERE")
let playerItem:AVPlayerItem = AVPlayerItem(url: url!)
player = AVPlayer(playerItem: playerItem)

To change the progress value based on the audio playback time onLoad set the progress bar minimum value to zero and use the below code to change the value of labelCurrentTime and playbackSlider and to set the labelOverallDuration.

playbackSlider.minimumValue = 0

To get the overall duration of the audio

let duration : CMTime = playerItem.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)

labelOverallDuration.text = self.stringFromTimeInterval(interval: seconds)

To get the current duration of the audio

let currentDuration : CMTime = playerItem.currentTime()
let currentSeconds : Float64 = CMTimeGetSeconds(currentDuration)

labelCurrentTime.text = self.stringFromTimeInterval(interval: currentSeconds)

Set the playerSlider maximumValue with the overAllDuration

playbackSlider.maximumValue = Float(seconds)
playbackSlider.isContinuous = true

Call the addPeriodicTimeObserver to continuously change the value of the current time and progress value and to keep track of player is playing or buffering if player is buffering hide the the button and show the loader else wise versa.

player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.player!.currentItem?.status == .readyToPlay {
let time : Float64 = CMTimeGetSeconds(self.player!.currentTime());
self.playbackSlider.value = Float ( time );
self.labelCurrentTime.text = self.stringFromTimeInterval(interval: time)
}

let playbackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp
if playbackLikelyToKeepUp == false {
print(“IsBuffering”)
self.ButtonPlay.isHidden = true
self.loadingView.isHidden = false
} else {
//stop the activity indicator
print(“Buffering completed”)
self.ButtonPlay.isHidden = false
self.loadingView.isHidden = true
}
}

To seek backWard use the following code in the button action

@IBAction func seekBackWards(_ sender: Any) {
if player == nil { return }
let playerCurrenTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrenTime — seekDuration
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()
}

To seek forward use the below code

@IBAction func seekForward(_ sender: Any) {
if player == nil { return }
if let duration = player!.currentItem?.duration {
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
let newTime = playerCurrentTime + seekDuration
if newTime < CMTimeGetSeconds(duration)
{
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player!.seek(to: selectedTime)
}
player?.pause()
player?.play()
}
}

And code for the play audio

@IBAction func playButton(_ sender: Any) {
print(“play Button”)
if player?.rate == 0 {
player!.play()
self.ButtonPlay.isHidden = true
self.loadingView.isHidden = false
ButtonPlay.setImage(UIImage(named: “ic_pause”), for: UIControl.State.normal)
} else {
player!.pause()
ButtonPlay.setImage(UIImage(named: “ic_play”), for: UIControl.State.normal)
}
}

Below is the function to convert time interval to string

   func stringFromTimeInterval(interval: TimeInterval) -> String {

let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let hours = (interval / 3600)
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}

If you want to change the progress value use the following code in view did load

playbackSlider.addTarget(self, action: #selector(playbackSliderValueChanged(_:)), for: .valueChanged)

and the change the value of the slider and player starts from

 @objc func playbackSliderValueChanged(_ playbackSlider:UISlider) {
let seconds : Int64 = Int64(playbackSlider.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)

player!.seek(to: targetTime)

if player!.rate == 0
{
player?.play()
}
}

add an observer to check player has completed playing the audio, not if complete change the button state

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

change the button state to play when the player completed playing.

@objc func finishedPlaying( _ myNotification:NSNotification) {
ButtonPlay.setImage(UIImage(named: "ic_play"), for: UIControl.State.normal)
}

Thanks for reading.

GitHub project link.

--

--

No responses yet