use std::{
ptr::NonNull,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex, Weak,
},
};
use block2::RcBlock;
use objc2_avf_audio::{
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, StreamInner};
use crate::{
host::{emit_error, latch::Latch, ErrorCallbackArc},
Error, ErrorKind,
};
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);
}
}
}
fn user_info_number(notification: &NSNotification, key: Option<&NSString>) -> Option<usize> {
let user_info = notification.userInfo()?;
let value = user_info.objectForKey(key?)?;
Some(value.downcast::<NSNumber>().ok()?.unsignedIntegerValue())
}
type LatencyRefresh = (Arc<AtomicUsize>, bool);
fn route_change_error(notification: &NSNotification) -> Option<Error> {
let key = unsafe { AVAudioSessionRouteChangeReasonKey };
let reason = AVAudioSessionRouteChangeReason(user_info_number(notification, key)?);
match reason {
AVAudioSessionRouteChangeReason::OldDeviceUnavailable => Some(Error::with_message(
ErrorKind::DeviceChanged,
"Audio route changed",
)),
AVAudioSessionRouteChangeReason::CategoryChange
| AVAudioSessionRouteChangeReason::Override
| AVAudioSessionRouteChangeReason::RouteConfigurationChange => Some(Error::with_message(
ErrorKind::StreamInvalidated,
"Audio route changed",
)),
AVAudioSessionRouteChangeReason::NoSuitableRouteForCategory => Some(Error::with_message(
ErrorKind::DeviceNotAvailable,
"No suitable audio route for the session category",
)),
_ => None,
}
}
pub(super) struct SessionEventManager {
latch: Latch,
observers: Vec<
objc2::rc::Retained<objc2::runtime::ProtocolObject<dyn objc2::runtime::NSObjectProtocol>>,
>,
}
unsafe impl Send for SessionEventManager {}
unsafe impl Sync for SessionEventManager {}
impl SessionEventManager {
pub(super) fn new(
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();
{
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 interruption_type_key = unsafe { AVAudioSessionInterruptionTypeKey };
let Some(kind) = user_info_number(notif, interruption_type_key) else {
return;
};
if AVAudioSessionInterruptionType(kind) == AVAudioSessionInterruptionType::Began {
with_stream(&stream, StreamInner::stop_for_interruption);
return;
}
let interruption_option_key = unsafe { AVAudioSessionInterruptionOptionKey };
let options = AVAudioSessionInterruptionOptions(
user_info_number(notif, interruption_option_key).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();
let block = RcBlock::new(move |notif: NonNull<NSNotification>| {
if w.is_released() {
if let Some((frames, is_input)) = &latency_refresh {
let depth = if *is_input {
input_latency_frames()
} else {
output_latency_frames()
};
frames.store(depth, Ordering::Relaxed);
}
let notif = unsafe { notif.as_ref() };
if let Some(err) = route_change_error(notif) {
emit_error(&cb, err);
}
}
});
if let Some(name) = unsafe { AVAudioSessionRouteChangeNotification } {
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();
let block = RcBlock::new(move |_: NonNull<NSNotification>| {
if w.is_released() {
emit_error(
&cb,
Error::with_message(
ErrorKind::DeviceNotAvailable,
"Audio media services were lost",
),
);
}
});
if let Some(name) = unsafe { AVAudioSessionMediaServicesWereLostNotification } {
let observer = unsafe {
nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block)
};
observers.push(observer);
}
}
{
let cb = error_callback.clone();
let w = waiter;
let block = RcBlock::new(move |_: NonNull<NSNotification>| {
if w.is_released() {
emit_error(
&cb,
Error::with_message(
ErrorKind::StreamInvalidated,
"Audio media services were reset",
),
);
}
});
if let Some(name) = unsafe { AVAudioSessionMediaServicesWereResetNotification } {
let observer = unsafe {
nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block)
};
observers.push(observer);
}
}
Self { latch, observers }
}
pub(super) fn signal_ready(&self) {
self.latch.release();
}
}
impl Drop for SessionEventManager {
fn drop(&mut self) {
let nc = NSNotificationCenter::defaultCenter();
for observer in &self.observers {
unsafe { nc.removeObserver(observer.as_ref()) };
}
}
}