AVAudioPlayer «забывает», что он воспроизводится при запуске MPRemoteCommand

Я пытаюсь воспроизвести аудиофайл и управлять его воспроизведением через Центр удаленного управления, доступный на экране блокировки.

Если я сделаю следующее:

  • Начать воспроизведение
  • Приостановить воспроизведение
  • Блокировка устройства
  • Начать воспроизведение с заблокированного экрана (MPRemoteCommandCenter)

В этом случае невозможно приостановить воспроизведение с заблокированного экрана. Кнопка мерцает и ничего не происходит.

Как я могу это исправить?

Более подробная информация ниже:

  • Похоже, что при попытке приостановить звук AVAudioPlayer возвращает false для audioPlayer.isPlaying.
  • Это происходит на iOS13.1 на моих iPhoneXR, iPhoneSE и iPhone8. У меня нет других устройств, с которыми можно было бы тестировать
  • Журналы показывают, что AVAudioPlayer.isPlaying изначально возвращает true при запуске воспроизведения, но впоследствии возвращает false. CurrentTime проигрывателя также застревает примерно в то время, когда было начато воспроизведение.
  • Весь мой контроллер представления находится ниже (~ 100 строк). Это минимум, необходимый для воспроизведения проблемы.
  • Этот пример проекта, демонстрирующий ошибку, также доступен на Github здесь.

импортировать UIKit импортировать 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
    }
}

Это регистрирует следующее (с добавленными моими // комментариями):

.......................
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. ='(

Я в своем уме. Помогите мне StackOverflow - ты моя единственная надежда.

Правки: я тоже пробовал

Всегда возвращаю .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
}

Игнорирование состояния audioPlayer и выполнение требований удаленного командного центра.

@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
}

В обоих случаях ошибка сохраняется при первом нажатии pause на экране блокировки. Последующие нажатия сбрасывают звук в исходное положение паузы, а затем работают в обычном режиме.

Обновлять

  • Замена AVAudioPlayer на AVPlayer, похоже, решает проблему полностью! Я вполне уверен, что сейчас это ошибка в AVAudioPlayer.
  • Чтобы переключиться на AVPlayer, необходимо выполнить этот общедоступный файл diff.
  • Я использовал один из своих билетов с вопросами для разработчиков и отправил этот вопрос в Apple. Я отправлю ответ, когда получу от них ответ.

Обновление 2

  • Служба поддержки Apple Dev подтвердила, что по состоянию на 5 декабря 2019 года обходного пути решения этой проблемы не существует. Я отправил вопрос на feedbackassistant.apple.com и обновлю этот ответ, когда что-то изменится.

person RP-3    schedule 21.11.2019    source источник
comment
@matt Я просто попробовал это по вашему предложению, и это не сработало. Я добавляю правку к вопросу с точным кодом, который я пробовал.   -  person RP-3    schedule 21.11.2019
comment
@matt Я тоже только что попробовал то, что ты мне говоришь. Правка добавлена ​​к моему вопросу. Первое нажатие на кнопку паузы на заблокированном экране теперь просто игнорируется при первом нажатии. Прошу прощения за недоразумение и благодарен за вашу помощь.   -  person RP-3    schedule 21.11.2019


Ответы (1)


Это действительно ошибка в AVAudioPlayer.

Другой обходной путь, если вы не хотите переключаться на AVPlayer, - это просто проверить, играет ли игра перед паузой, а если нет, вызвать play непосредственно перед паузой. Это некрасиво, но работает:

if (!self.player.isPlaying) [self.player play];
[self.player pause];
person RunLoop    schedule 24.02.2021