Search code examples
iosswiftxcodeavplayerswift5

Swift 5: AVPlayer layers multiple audio files on each other


I'm building a Swift 5 music app where a table view loads and lists multiple objects from a JSON API. After clicking on an entry, the navigator calls the detail view for the selected item.

Below, you can see my detail view controller, called BroadcastDetailViewController. As you can see there is a playBroadcastButton which'll play the audio file and setup the AVAudioSession.

So far so good. But after I've implemented the possibility to scrub through the audio file via control center scrubber (commandCenter.changePlaybackPositionCommand.addTarget) and go back to my table view controller and select another item to play it's audio file, the AVPlayer layers multiple audio files on each other and plays them simultaneously. This is a behavior I don't want. Before I implemented the control buttons, the whole audio file replacement process worked fine.

My question is, why does the player suddenly play another file on top of the other, instead of muting/ducking the old one and replace it with the new one?

class BroadcastDetailViewController: UIViewController {

    var player = AVPlayer()
    var playerItem: AVPlayerItem!

    var broadcast:Broadcasts?

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!

    @IBAction func playBroadcastButton(_ sender: Any) {
        player.play()
        setupAVAudioSession()
    }

    private func setupAVAudioSession() {
        do {
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
            try AVAudioSession.sharedInstance().setActive(true)
            debugPrint("AVAudioSession is Active and Category Playback is set")
            UIApplication.shared.beginReceivingRemoteControlEvents()
            setupCommandCenter()
        } catch {
            debugPrint("Error: \(error)")
        }
    }

    private func setupCommandCenter() {

        // Meta
        var nowPlayingInfo = [String : Any]()

        nowPlayingInfo[MPMediaItemPropertyTitle] = broadcast?.title ?? "Radio Bass"
        nowPlayingInfo[MPMediaItemPropertyArtist] = "Radio Bass"
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "Radio Bass."
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playerItem.currentTime().seconds
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo

        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.playCommand.isEnabled = true
        commandCenter.pauseCommand.isEnabled = true
        commandCenter.playCommand.addTarget { [weak self] (event) -> MPRemoteCommandHandlerStatus in
            self?.player.play()
            return .success
        }
        commandCenter.pauseCommand.addTarget { [weak self] (event) -> MPRemoteCommandHandlerStatus in
            self?.player.pause()
            return .success
        }

        // Scrubber
        commandCenter.changePlaybackPositionCommand.addTarget { event in
            let seconds = (event as? MPChangePlaybackPositionCommandEvent)?.positionTime ?? 0
            let time = CMTime(seconds: seconds, preferredTimescale: 1)
            self.player.seek(to: time)
            return .success
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let urlAudioString = broadcast?.audio

        playerItem = AVPlayerItem(url: URL(string: urlAudioString!)!)
        player = AVPlayer(playerItem: playerItem)

        title = broadcast?.title

        let urlImageString = broadcast?.image
        let urlImage = URL(string: urlImageString!)

        titleLabel.text = broadcast?.title
        imageView.load(url: urlImage!)
    }
}

Solution

  • Release/free and stop AVPlayer and AVPlayerItem before going back to your table view controller

    avPlayer.pause()
    avPlayer.cancelPendingPrerolls() // stops network requests
    avPlayer.replaceCurrentItem(with: nil)