-
Notifications
You must be signed in to change notification settings - Fork 523
ios: recover the stream after an audio session interruption #1289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -190,11 +190,8 @@ impl DeviceTrait for Device { | |
| let latency_frames = Arc::new(AtomicUsize::new(input_latency_frames())); | ||
|
|
||
| let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); | ||
| let session_manager = SessionEventManager::new( | ||
| error_callback.clone(), | ||
| Latch::new(), | ||
| Some((latency_frames.clone(), true)), | ||
| ); | ||
| let session_error_callback = error_callback.clone(); | ||
| let session_latency_frames = latency_frames.clone(); | ||
|
|
||
| // Set up input callback | ||
| setup_input_callback( | ||
|
|
@@ -208,13 +205,21 @@ impl DeviceTrait for Device { | |
| }, | ||
| )?; | ||
|
|
||
| let stream = Stream::new( | ||
| StreamInner { | ||
| playing: false, | ||
| audio_unit, | ||
| }, | ||
| session_manager, | ||
| let inner = Arc::new(Mutex::new(StreamInner { | ||
| playing: false, | ||
| interrupted: false, | ||
| audio_unit, | ||
| })); | ||
| let session_manager = SessionEventManager::new( | ||
| session_error_callback, | ||
| Latch::new(), | ||
| Some((session_latency_frames, true)), | ||
| Arc::downgrade(&inner), | ||
| ); | ||
| let stream = Stream { | ||
| inner, | ||
| session_manager, | ||
| }; | ||
| stream.signal_ready(); | ||
| Ok(stream) | ||
| } | ||
|
|
@@ -245,11 +250,8 @@ impl DeviceTrait for Device { | |
| let latency_frames = Arc::new(AtomicUsize::new(output_latency_frames())); | ||
|
|
||
| let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); | ||
| let session_manager = SessionEventManager::new( | ||
| error_callback.clone(), | ||
| Latch::new(), | ||
| Some((latency_frames.clone(), false)), | ||
| ); | ||
| let session_error_callback = error_callback.clone(); | ||
| let session_latency_frames = latency_frames.clone(); | ||
|
|
||
| // Set up output callback | ||
| setup_output_callback( | ||
|
|
@@ -263,31 +265,32 @@ impl DeviceTrait for Device { | |
| }, | ||
| )?; | ||
|
|
||
| let stream = Stream::new( | ||
| StreamInner { | ||
| playing: false, | ||
| audio_unit, | ||
| }, | ||
| session_manager, | ||
| let inner = Arc::new(Mutex::new(StreamInner { | ||
| playing: false, | ||
| interrupted: false, | ||
| audio_unit, | ||
| })); | ||
| let session_manager = SessionEventManager::new( | ||
| session_error_callback, | ||
| Latch::new(), | ||
| Some((session_latency_frames, false)), | ||
| Arc::downgrade(&inner), | ||
| ); | ||
| let stream = Stream { | ||
| inner, | ||
| session_manager, | ||
| }; | ||
| stream.signal_ready(); | ||
| Ok(stream) | ||
| } | ||
| } | ||
|
|
||
| pub struct Stream { | ||
| inner: Mutex<StreamInner>, | ||
| inner: Arc<Mutex<StreamInner>>, | ||
| session_manager: SessionEventManager, | ||
| } | ||
|
|
||
| impl Stream { | ||
| fn new(inner: StreamInner, session_manager: SessionEventManager) -> Self { | ||
| Self { | ||
| inner: Mutex::new(inner), | ||
| session_manager, | ||
| } | ||
| } | ||
|
|
||
| fn signal_ready(&self) { | ||
| self.session_manager.signal_ready(); | ||
| } | ||
|
|
@@ -341,9 +344,33 @@ impl StreamTrait for Stream { | |
|
|
||
| struct StreamInner { | ||
| playing: bool, | ||
| /// Set while an interruption is what stopped the unit, so only those resume on its end. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove comments where the code is self-explanatory. |
||
| interrupted: bool, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of an 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. |
||
| audio_unit: AudioUnit, | ||
| } | ||
|
|
||
| impl StreamInner { | ||
| /// The OS already stopped the unit; resync `playing` so a later `play` actually starts it. | ||
| fn stop_for_interruption(&mut self) { | ||
| if !self.playing { | ||
| return; | ||
| } | ||
| let _ = self.audio_unit.stop(); | ||
| self.playing = false; | ||
| self.interrupted = true; | ||
| } | ||
|
|
||
| /// Only resumes what the interruption stopped; a stream the caller had paused stays paused. | ||
| fn resume_after_interruption(&mut self) { | ||
| if !std::mem::take(&mut self.interrupted) { | ||
| return; | ||
| } | ||
| if self.audio_unit.start().is_ok() { | ||
| self.playing = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn create_audio_unit() -> Result<AudioUnit, coreaudio::Error> { | ||
| AudioUnit::new_uninitialized(coreaudio::audio_unit::IOType::RemoteIO) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,54 @@ | ||
| //! Monitors AVAudioSession lifecycle events and reports them as stream errors. | ||
| //! Monitors AVAudioSession lifecycle events, recovering the stream from an interruption and | ||
| //! reporting the rest as stream errors. | ||
|
|
||
| use std::{ | ||
| ptr::NonNull, | ||
| sync::{ | ||
| atomic::{AtomicUsize, Ordering}, | ||
| Arc, | ||
| Arc, Mutex, Weak, | ||
| }, | ||
| }; | ||
|
|
||
| use block2::RcBlock; | ||
| use objc2::runtime::AnyObject; | ||
| use objc2_avf_audio::{ | ||
| AVAudioSessionMediaServicesWereLostNotification, | ||
| AVAudioSession, AVAudioSessionInterruptionNotification, AVAudioSessionInterruptionOptionKey, | ||
| AVAudioSessionInterruptionOptions, AVAudioSessionInterruptionType, | ||
| AVAudioSessionInterruptionTypeKey, AVAudioSessionMediaServicesWereLostNotification, | ||
| AVAudioSessionMediaServicesWereResetNotification, AVAudioSessionRouteChangeNotification, | ||
| AVAudioSessionRouteChangeReason, AVAudioSessionRouteChangeReasonKey, | ||
| }; | ||
| use objc2_foundation::{NSNotification, NSNotificationCenter, NSNumber, NSString}; | ||
|
|
||
| use super::{input_latency_frames, output_latency_frames}; | ||
| use super::{input_latency_frames, output_latency_frames, StreamInner}; | ||
| use crate::{ | ||
|
benface marked this conversation as resolved.
|
||
| host::{emit_error, latch::Latch, ErrorCallbackArc}, | ||
| Error, ErrorKind, | ||
| }; | ||
|
|
||
| /// Runs `f` against the stream, if it is still alive and its lock is intact. | ||
| fn with_stream(stream: &Weak<Mutex<StreamInner>>, f: impl FnOnce(&mut StreamInner)) { | ||
| if let Some(inner) = stream.upgrade() { | ||
| if let Ok(mut inner) = inner.lock() { | ||
| f(&mut inner); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Reads the number stored under `key` in a notification's `userInfo`. | ||
| unsafe fn user_info_number(notification: &NSNotification, key: Option<&NSString>) -> Option<usize> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this doesn't need |
||
| let user_info = notification.userInfo()?; | ||
| let value = user_info.objectForKey(key?)?; | ||
| Some(value.downcast::<NSNumber>().ok()?.unsignedIntegerValue()) | ||
| } | ||
|
|
||
| /// Shared buffer-depth value to refresh on route changes, paired with `is_input` to select the | ||
| /// input or output latency. `true` means an input stream. | ||
| type LatencyRefresh = (Arc<AtomicUsize>, bool); | ||
|
|
||
| unsafe fn route_change_error(notification: &NSNotification) -> Option<Error> { | ||
| let user_info = notification.userInfo()?; | ||
| let key = AVAudioSessionRouteChangeReasonKey?; | ||
| let dict = unsafe { user_info.cast_unchecked::<NSString, AnyObject>() }; | ||
| let value = dict.objectForKey(key)?; | ||
| let number = value.downcast_ref::<NSNumber>()?; | ||
| let reason = AVAudioSessionRouteChangeReason(number.unsignedIntegerValue()); | ||
| let reason = AVAudioSessionRouteChangeReason(unsafe { | ||
| user_info_number(notification, AVAudioSessionRouteChangeReasonKey)? | ||
| }); | ||
| match reason { | ||
| AVAudioSessionRouteChangeReason::OldDeviceUnavailable => Some(Error::with_message( | ||
| ErrorKind::DeviceChanged, | ||
|
|
@@ -73,11 +88,50 @@ impl SessionEventManager { | |
| error_callback: ErrorCallbackArc, | ||
| latch: Latch, | ||
| latency_refresh: Option<LatencyRefresh>, | ||
| stream: Weak<Mutex<StreamInner>>, | ||
| ) -> Self { | ||
| let nc = NSNotificationCenter::defaultCenter(); | ||
| let mut observers = Vec::new(); | ||
| let waiter = latch.waiter(); | ||
|
|
||
| // The OS stops the unit itself, and the session it stops is inactive on the way back. | ||
| { | ||
| let w = waiter.clone(); | ||
| let stream = stream.clone(); | ||
| let block = RcBlock::new(move |notif: NonNull<NSNotification>| { | ||
| if !w.is_released() { | ||
| return; | ||
| } | ||
| let notif = unsafe { notif.as_ref() }; | ||
| let Some(kind) = | ||
| (unsafe { user_info_number(notif, AVAudioSessionInterruptionTypeKey) }) | ||
| else { | ||
| return; | ||
| }; | ||
| if AVAudioSessionInterruptionType(kind) == AVAudioSessionInterruptionType::Began { | ||
| with_stream(&stream, StreamInner::stop_for_interruption); | ||
| return; | ||
| } | ||
| let options = AVAudioSessionInterruptionOptions( | ||
| unsafe { user_info_number(notif, AVAudioSessionInterruptionOptionKey) } | ||
| .unwrap_or(0), | ||
| ); | ||
| if !options.contains(AVAudioSessionInterruptionOptions::ShouldResume) { | ||
| return; | ||
| } | ||
| let session = unsafe { AVAudioSession::sharedInstance() }; | ||
| if unsafe { session.setActive_error(true) }.is_ok() { | ||
| with_stream(&stream, StreamInner::resume_after_interruption); | ||
| } | ||
| }); | ||
| if let Some(name) = unsafe { AVAudioSessionInterruptionNotification } { | ||
| let observer = unsafe { | ||
| nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) | ||
| }; | ||
| observers.push(observer); | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let cb = error_callback.clone(); | ||
| let w = waiter.clone(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be shorter, like: "Streams now resume automatically when an audio session interruption ends."