ios: recover the stream after an audio session interruption - #1289
ios: recover the stream after an audio session interruption#1289benface wants to merge 2 commits into
Conversation
|
Confirmed end to end on an iPhone. The shipping app was pointed at this branch with its own |
roderickvd
left a comment
There was a problem hiding this comment.
Thanks for tracking this down and verifying on real hardware, that's a real bug.
Per CONTRIBUTING.md I don't mind AI usage, but you do need to hold it to the same bar as hand-written code, and some of the comments here read unedited, see inline. Couple of points on the interruption handling itself too.
And if you would also add a short entry to CHANGELOG.md?
f2c568b to
02ce116
Compare
A call, an alarm or Siri stops the audio unit through CoreAudio without going through cpal, so `StreamInner::playing` is left reading `true` for a unit that is no longer running. `play` then short-circuits on that flag, and the stream can never be resumed: the caller sees a successful `play` and silence, with no error to explain it. `AVAudioSessionInterruptionNotification` is now observed alongside the route-change and media-services notifications the manager already handles. Beginning an interruption stops the unit through the normal path, bringing the flag back in step with reality. When one ends carrying `ShouldResume`, the session is reactivated — it is left inactive, and a unit cannot start against an inactive session — and the unit started again, so a caller does not have to observe the notification itself just to get its stream back. Only streams the interruption stopped resume; one the caller had paused stays paused. Confirmed on device: an app pointed at this branch, with its own restart workaround and its interruption observer both removed, recovers its audio after an alarm is dismissed.
02ce116 to
079ea89
Compare
|
Thanks @roderickvd, you were right about Updated:
Also folded the two different ways of reading Confirmed on device again: app pointed at this branch with both its own workaround and its interruption observer removed, so recovery had to come from cpal alone. Music comes back after dismissing an alarm. |
roderickvd
left a comment
There was a problem hiding this comment.
Thanks for processing the previous round. Having worked through the various states, I think we need some further changes to prevent race conditions.
|
|
||
| struct StreamInner { | ||
| playing: bool, | ||
| /// Set while an interruption is what stopped the unit, so only those resume on its end. |
There was a problem hiding this comment.
Remove comments where the code is self-explanatory.
| let mut stream = self.inner.lock().map_err(|_| { | ||
| Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") | ||
| })?; | ||
| if stream.playing { |
There was a problem hiding this comment.
pause now needs to set stream.interrupted = false. Otherwise, calling pause during an interruption would have the end-of-interruption event restart the stream when you don't want to.
| } | ||
|
|
||
| /// Reads the number stored under `key` in a notification's `userInfo`. | ||
| unsafe fn user_info_number(notification: &NSNotification, key: Option<&NSString>) -> Option<usize> { |
There was a problem hiding this comment.
I think this doesn't need unsafe any longer?
| - **AudioWorklet**: Fix stale audio output when a data callback wrote a partial buffer. | ||
| - **CoreAudio**: Default-output streams now report xrun status. | ||
| - **CoreAudio**: Fix stale audio output when a data callback wrote a partial buffer. | ||
| - **iOS**: Streams stopped by an audio session interruption now resume when it ends, instead of staying silent until rebuilt. |
There was a problem hiding this comment.
Could this be shorter, like: "Streams now resume automatically when an audio session interruption ends."
| struct StreamInner { | ||
| playing: bool, | ||
| /// Set while an interruption is what stopped the unit, so only those resume on its end. | ||
| interrupted: bool, |
There was a problem hiding this comment.
Instead of an interrupted field next to playing, we should probably use an enum like:
enum PlaybackState {
Stopped,
Playing,
Interrupted,
}This will also prevent race conditions when one flag is written, the other not yet, and one of the closures is called.
The problem
On iOS an audio session interruption — a call, an alarm, Siri — stops the audio unit through CoreAudio without going through cpal.
StreamInner::playingis left readingtruefor a unit that is no longer running, soStreamTrait::playshort-circuits:An interrupted stream therefore can never be resumed for the lifetime of the process:
play()returnsOk(())and produces silence.AVAudioSessionInterruptionNotificationis not among the notificationsSessionEventManagerwatches, so nothing is reported either and the caller has no way to discover why. Reactivating the session withsetActive:YESdoes not help on its own — the session comes back and the unit stays stopped.The change
Observe
AVAudioSessionInterruptionNotificationalongside the route-change and media-services notifications already handled. On the beginning of an interruption, stop the stream through the normal path — which brings the flag back in step so a laterplaystarts the unit again — and report aStreamInvalidatederror.Resuming stays the caller's decision, since Apple's guidance is that the app reads
AVAudioSessionInterruptionOptionShouldResume, so the stream is left stopped rather than restarted here.SessionEventManageris built before theStream, so it takes a slot thatStream::newfills with aWeak<Mutex<StreamInner>>. The observer blocks already gate on the readiness latch, which is released later still, so the slot is always populated by the time one can act — reusing the existing ordering invariant rather than adding a second.If you would rather not touch the state layout, a reporting-only variant is a much smaller diff, though callers would still be unable to recover.
Verification
Reproduced and confirmed on an iPhone against a shipping app: the stop-then-start this performs is what restores audio after an interruption.
cargo check --target aarch64-apple-ioscleancargo clippy --target aarch64-apple-ios --libcleancargo fmtclean--all-targetstest errors on this target (default_output_device,OutputCallbackInfo,SampleRate) are pre-existing onmasterand unrelated