Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AVAudioPlayer 'forgets' that its playing when triggered by MPRemoteCommand

I'm trying to play an audiofile and control it's playback via the Remote Command Centre available on the lock screen.

If I do the following:

  • Begin playback
  • Pause playback
  • Lock device
  • Begin playback from lockscreen (MPRemoteCommandCenter)

It is then impossible to pause playback from lockscreen. The button flickers and nothing happens.

How can I fix this?

Further details below:

  • It appears that when attempting to pause the audio, the AVAudioPlayer returns 'false' for audioPlayer.isPlaying.
  • This occurs on iOS13.1 on my iPhoneXR, iPhoneSE and iPhone8. I have no other devices to test against
  • The logs indicate that AVAudioPlayer.isPlaying initially returns true when playback is started, but subsequently returns false. The player's currentTime also appears stuck at around the time playback was started.
  • My entire view controller is below (~100 lines). This is the minimum necessary to reproduce the problem.
  • The this example project demonstrating the error is also available on Github here.

import UIKit import MediaPlayer

class ViewController: UIViewController {
    @IBOutlet weak var playPauseButton: UIButton!
    @IBAction func playPauseButtonTap(_ sender: Any) {
        if self.audioPlayer.isPlaying {
            pause()
        } else {
            play()
        }
    }

    private var audioPlayer: AVAudioPlayer!
    private var hasPlayed = false

    override func viewDidLoad() {
        super.viewDidLoad()

        let fileUrl = Bundle.main.url(forResource: "temp/intro", withExtension: ".mp3")
        try! self.audioPlayer = AVAudioPlayer(contentsOf: fileUrl!)
        let audioSession = AVAudioSession.sharedInstance()
        do { // play on speakers if headphones not plugged in
            try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
        } catch let error as NSError {
            print("Override headphones failed, probably because none are available: \(error.localizedDescription)")
        }
        do {
            try audioSession.setCategory(.playback, mode: .spokenAudio)
            try audioSession.setActive(true)
        } catch let error as NSError {
            print("Warning: Setting audio category to .playback|.spokenAudio failed: \(error.localizedDescription)")
        }
        playPauseButton.setTitle("Play", for: .normal)
    }

    func play() {
        playPauseButton.setTitle("Pause", for: .normal)
        self.audioPlayer.play()
        if(!hasPlayed){
            self.setupRemoteTransportControls()
            self.hasPlayed = true
        }
    }

    func pause() {
        playPauseButton.setTitle("Play", for: .normal)
        self.audioPlayer.pause()
    }

    // MARK: Remote Transport Protocol
    @objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
        print(".......................")
        print(self.audioPlayer.currentTime)
        let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
        print("\(address) not playing: \(!self.audioPlayer.isPlaying)")
        guard !self.audioPlayer.isPlaying else { return .commandFailed }
        print("attempting to play")
        let success = self.audioPlayer.play()
        print("play() invoked with success \(success)")
        print("now playing \(self.audioPlayer.isPlaying)")
        return success ? .success : .commandFailed
    }

    @objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
        print(".......................")
        print(self.audioPlayer.currentTime)
        let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
        print("\(address) playing: \(self.audioPlayer.isPlaying)")
        guard self.audioPlayer.isPlaying else { return .commandFailed }
        print("attempting to pause")
        self.pause()
        print("pause() invoked")
        return .success
    }

    private func setupRemoteTransportControls() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.playCommand.addTarget(self, action: #selector(self.handlePlay))
        commandCenter.pauseCommand.addTarget(self, action: #selector(self.handlePause))

        var nowPlayingInfo = [String : Any]()
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "Major title"
        nowPlayingInfo[MPMediaItemPropertyTitle] = "Minor Title"
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.audioPlayer.duration
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
}

This logs the following (with my // comments added):

.......................
1.438140589569161 // audio was paused here
0x0000000283361cc0 not playing: true // player correctly says its not playing
attempting to play // so it'll start to play
play() invoked with success true // play() successfully invoked
now playing true // and the player correctly reports it's playing
.......................
1.4954875283446711 // The player thinks it's being playing for about half a second
0x0000000283361cc0 playing: false // and has now paused??? WTF?
.......................
1.4954875283446711 // but there's definitely sound coming from the speakers. It has **NOT** paused. 
0x0000000283361cc0 playing: false // yet it thinks it's paused?
// note that the memory addresses are the same. This seems to be the same player. ='(

I'm at my wits' end. Help me StackOverflow—You're my only hope.

Edits: I've also tried

Always returning .success

@objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
    guard !self.audioPlayer.isPlaying else { return .success }
    self.audioPlayer.play()
    return .success
}

@objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
    print(self.audioPlayer.isPlaying)
    guard self.audioPlayer.isPlaying else { return .success }
    self.pause()
    return .success
}

Ignoring the audioPlayer state and just doing as the remote command centre says

@objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
    self.audioPlayer.play()
    MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
    MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
    return .success
}

@objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
    self.audioPlayer.pause()
    MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
    MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
    return .success
}

Both of these result in the bug persisting the first time pause is tapped on the lock screen. Subsequent taps reset the audio to the original paused position and then work normally.

Update

  • Replacing AVAudioPlayer with AVPlayer appears to make the problem go away entirely! I'm reasonably sure this is a bug in AVAudioPlayer now.
  • The necessary steps to switch to AVPlayer are in this public diff
  • I've used one of my developer-question tickets and submitted this question to Apple. I'll post an answer when I hear back from them.

Update 2

  • Apple Dev Support confirmed that as of 5 Dec 2019 there's no known workaround for this issue. I've submitted an issue to feedbackassistant.apple.com and will update this answer when something changes.
like image 413
RP-3 Avatar asked Nov 06 '22 11:11

RP-3


1 Answers

This is indeed a bug in AVAudioPlayer.

Another workaround if you dont want to switch to AVPlayer is to simply check if playing before pausing and if not, call play just before pause. It's not pretty but it works:

if (!self.player.isPlaying) [self.player play];
[self.player pause];
like image 140
RunLoop Avatar answered Nov 14 '22 21:11

RunLoop