fix(bridge): serialize iOS audio lifecycle events
This commit is contained in:
@@ -23,7 +23,7 @@ thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
|
||||
|
||||
# Android: route `tracing` output to logcat so a user can see protocol
|
||||
# and audio diagnostics via `adb logcat -s chanora`. Also brings in the
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::time::Duration;
|
||||
|
||||
use flutter_rust_bridge::frb;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::frb_generated::StreamSink;
|
||||
@@ -42,6 +42,73 @@ fn task_join_error(task: &'static str, error: tokio::task::JoinError) -> BridgeE
|
||||
BridgeError::Unmapped(format!("join: {error}"))
|
||||
}
|
||||
|
||||
enum PlatformAudioEvent {
|
||||
RouteChanged(chanora_audio::AudioRoute),
|
||||
MediaServicesResetWithRoute(chanora_audio::AudioRoute),
|
||||
InterruptionBegan,
|
||||
InterruptionEnded { should_resume: bool },
|
||||
AudioOutputRoute(chanora_audio::AudioRoute),
|
||||
Lifecycle { state: String },
|
||||
}
|
||||
|
||||
impl PlatformAudioEvent {
|
||||
async fn process(self) {
|
||||
match self {
|
||||
Self::RouteChanged(route) => {
|
||||
if let Err(e) = session().ios_handle_route_change(route).await {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS route-change handling failed");
|
||||
}
|
||||
}
|
||||
Self::MediaServicesResetWithRoute(route) => {
|
||||
if let Err(e) = session().ios_handle_media_services_reset(route).await {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS media-services reset (with route) handling failed");
|
||||
}
|
||||
}
|
||||
Self::InterruptionBegan => {
|
||||
if let Err(e) = session().ios_handle_interruption_began().await {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS interruption-began handling failed");
|
||||
}
|
||||
}
|
||||
Self::InterruptionEnded { should_resume } => {
|
||||
if let Err(e) = session().ios_handle_interruption_ended(should_resume).await {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS interruption-ended handling failed");
|
||||
}
|
||||
}
|
||||
Self::AudioOutputRoute(route) => {
|
||||
if let Err(e) = session().ios_handle_route_change(route).await {
|
||||
warn!(target: "chanora_bridge", error = %e, "audio output route handling failed");
|
||||
}
|
||||
}
|
||||
Self::Lifecycle { state } => {
|
||||
session().record_lifecycle_event(&state).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn platform_audio_events() -> &'static mpsc::UnboundedSender<PlatformAudioEvent> {
|
||||
static TX: OnceLock<mpsc::UnboundedSender<PlatformAudioEvent>> = OnceLock::new();
|
||||
TX.get_or_init(|| {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<PlatformAudioEvent>();
|
||||
runtime().spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
event.process().await;
|
||||
}
|
||||
});
|
||||
tx
|
||||
})
|
||||
}
|
||||
|
||||
fn dispatch_platform_audio_event(event: PlatformAudioEvent) {
|
||||
if let Err(e) = platform_audio_events().send(event) {
|
||||
warn!(
|
||||
target: "chanora_bridge",
|
||||
error = %e,
|
||||
"ordered platform audio event dispatch failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn install_panic_diagnostic_hook() {
|
||||
static INSTALLED: OnceLock<()> = OnceLock::new();
|
||||
let _ = INSTALLED.get_or_init(|| {
|
||||
@@ -615,11 +682,7 @@ pub async fn is_connected() -> bool {
|
||||
/// Handle iOS AVAudioSession route changes (SDD-100).
|
||||
#[frb(sync)]
|
||||
pub fn handle_route_change(route: BridgeAudioRoute) {
|
||||
let result =
|
||||
runtime().block_on(async { session().ios_handle_route_change(route.into()).await });
|
||||
if let Err(e) = result {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS route-change handling failed");
|
||||
}
|
||||
dispatch_platform_audio_event(PlatformAudioEvent::RouteChanged(route.into()));
|
||||
}
|
||||
|
||||
/// Handle iOS AVAudioSession media-services reset with the current
|
||||
@@ -629,30 +692,19 @@ pub fn handle_route_change(route: BridgeAudioRoute) {
|
||||
#[frb(sync)]
|
||||
pub fn handle_media_services_reset_with_route(route_class: String) {
|
||||
let route = chanora_audio::AudioRoute::from_route_class(&route_class);
|
||||
let result =
|
||||
runtime().block_on(async { session().ios_handle_media_services_reset(route).await });
|
||||
if let Err(e) = result {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS media-services reset (with route) handling failed");
|
||||
}
|
||||
dispatch_platform_audio_event(PlatformAudioEvent::MediaServicesResetWithRoute(route));
|
||||
}
|
||||
|
||||
/// Handle iOS AVAudioSession interruption begin (SDD-101).
|
||||
#[frb(sync)]
|
||||
pub fn handle_interruption_began() {
|
||||
let result = runtime().block_on(async { session().ios_handle_interruption_began().await });
|
||||
if let Err(e) = result {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS interruption-began handling failed");
|
||||
}
|
||||
dispatch_platform_audio_event(PlatformAudioEvent::InterruptionBegan);
|
||||
}
|
||||
|
||||
/// Handle iOS AVAudioSession interruption end (SDD-101).
|
||||
#[frb(sync)]
|
||||
pub fn handle_interruption_ended(should_resume: bool) {
|
||||
let result =
|
||||
runtime().block_on(async { session().ios_handle_interruption_ended(should_resume).await });
|
||||
if let Err(e) = result {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS interruption-ended handling failed");
|
||||
}
|
||||
dispatch_platform_audio_event(PlatformAudioEvent::InterruptionEnded { should_resume });
|
||||
}
|
||||
|
||||
/// Set the focused/on-screen push-to-talk hold state.
|
||||
@@ -2139,14 +2191,10 @@ pub async fn set_ios_voice_processing_mode(
|
||||
/// Set the preferred audio output route (Android/iOS).
|
||||
#[frb(sync)]
|
||||
pub fn set_audio_output_route(route: BridgeAudioRoute) {
|
||||
runtime().block_on(async {
|
||||
let _ = session().ios_handle_route_change(route.into()).await;
|
||||
});
|
||||
dispatch_platform_audio_event(PlatformAudioEvent::AudioOutputRoute(route.into()));
|
||||
}
|
||||
/// Called from Flutter when the app enters background/foreground.
|
||||
#[frb(sync)]
|
||||
pub fn record_lifecycle_event(state: String) {
|
||||
runtime().block_on(async {
|
||||
session().record_lifecycle_event(&state).await;
|
||||
});
|
||||
dispatch_platform_audio_event(PlatformAudioEvent::Lifecycle { state });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user