fix(bridge): serialize iOS audio lifecycle events

This commit is contained in:
Edison Jwa
2026-05-31 22:29:38 +09:00
parent d39d6c78d2
commit c02d4e6df3
2 changed files with 75 additions and 27 deletions
+1 -1
View File
@@ -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
+74 -26
View File
@@ -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 });
}