stadiamaps / stadiamaps/ferrostar
iOS: AVAudioSession deactivated incorrectly
- Dominant language
- Kotlin
- Stars
- 419
- Forks
- 81
- Avg merge
- 6d 1h
- Merged PRs (30d)
- 7
Description
https://github.com/stadiamaps/ferrostar/blob/main/apple/Sources/FerrostarCore/Speech/SpokenInstructionObserver.swift#L76-L94
This code is incorrect and results in AVAudioSession not being deactivated properly in some cases. As a consequence, user audio playback such as Apple Music or Spotify might remain at a dimmed volume or is fully interrupted as in the case of Podcasts.
There are two core problems.
1. The code attempts to poll every 500 ms and check whether AVSpeechSynthesizer isSpeaking. However, there may be a period between two queued utterances when isSpeaking is false. It seems the assumption was that isSpeaking would remain true if utterances were still queued. This is not the case. The property is only true while the synthesizer is actually speaking. This issue becomes easier to reproduce if you set utterance.preUtteranceDelay to a large value. In that case, it is very likely that the code will hit the delay and deactivate the audio session prematurely. The bug arises because the audio session is deactivated while utterances are still queued, and the code assumes that the session has been successfully deactivated.
2. AVSpeechSynthesizer automatically activates the audio session each time it needs to speak. This is not clearly stated in the documentation, but it is easy to verify in practice, and it was explicitly described in a WWDC 2018 session that is no longer available. However, here is a transcript where this behavior is mentioned: [https://devstreaming-cdn.apple.com/videos/wwdc/2018/236mwbxbxjfsvns4jan/236/236_avspeechsynthesizer_making_ios_talk.pdf?dl=1](https://devstreaming-cdn.apple.com/videos/wwdc/2018/236mwbxbxjfsvns4jan/236/236_avspeechsynthesizer_making_ios_talk.pdf?dl=1). Once your code deactivates the session prematurely while queued utterances remain, the synthesizer will activate the session again, and your logic will not attempt to deactivate it afterward.
This results in system audio remaining dimmed or fully interrupted.
A better solution might be to use the AVSpeechSynthesizer delegate and deactivate the session in the didFinish callback. There is no need to manually activate the session because the synthesizer already does that internally, although you may choose to handle activation if this partially documented behavior ever changes.
My other, half-related suggestion would be to provide `SpokenInstructionObserver` protocol so developers can customize it. E.g. I'd prefer just to dim the volume and never interrupt audio (my subjective preferrence). This class is rather simple, so developers could tune it to their needs.
Contributor guide
Assessment
This issue has not been assessed yet.