fix(bridge): serialize iOS audio lifecycle events
This commit is contained in:
@@ -23,7 +23,7 @@ thiserror.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
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
|
# Android: route `tracing` output to logcat so a user can see protocol
|
||||||
# and audio diagnostics via `adb logcat -s chanora`. Also brings in the
|
# 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 flutter_rust_bridge::frb;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::{broadcast, mpsc};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::frb_generated::StreamSink;
|
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}"))
|
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() {
|
fn install_panic_diagnostic_hook() {
|
||||||
static INSTALLED: OnceLock<()> = OnceLock::new();
|
static INSTALLED: OnceLock<()> = OnceLock::new();
|
||||||
let _ = INSTALLED.get_or_init(|| {
|
let _ = INSTALLED.get_or_init(|| {
|
||||||
@@ -615,11 +682,7 @@ pub async fn is_connected() -> bool {
|
|||||||
/// Handle iOS AVAudioSession route changes (SDD-100).
|
/// Handle iOS AVAudioSession route changes (SDD-100).
|
||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn handle_route_change(route: BridgeAudioRoute) {
|
pub fn handle_route_change(route: BridgeAudioRoute) {
|
||||||
let result =
|
dispatch_platform_audio_event(PlatformAudioEvent::RouteChanged(route.into()));
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle iOS AVAudioSession media-services reset with the current
|
/// Handle iOS AVAudioSession media-services reset with the current
|
||||||
@@ -629,30 +692,19 @@ pub fn handle_route_change(route: BridgeAudioRoute) {
|
|||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn handle_media_services_reset_with_route(route_class: String) {
|
pub fn handle_media_services_reset_with_route(route_class: String) {
|
||||||
let route = chanora_audio::AudioRoute::from_route_class(&route_class);
|
let route = chanora_audio::AudioRoute::from_route_class(&route_class);
|
||||||
let result =
|
dispatch_platform_audio_event(PlatformAudioEvent::MediaServicesResetWithRoute(route));
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle iOS AVAudioSession interruption begin (SDD-101).
|
/// Handle iOS AVAudioSession interruption begin (SDD-101).
|
||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn handle_interruption_began() {
|
pub fn handle_interruption_began() {
|
||||||
let result = runtime().block_on(async { session().ios_handle_interruption_began().await });
|
dispatch_platform_audio_event(PlatformAudioEvent::InterruptionBegan);
|
||||||
if let Err(e) = result {
|
|
||||||
warn!(target: "chanora_bridge", error = %e, "iOS interruption-began handling failed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle iOS AVAudioSession interruption end (SDD-101).
|
/// Handle iOS AVAudioSession interruption end (SDD-101).
|
||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn handle_interruption_ended(should_resume: bool) {
|
pub fn handle_interruption_ended(should_resume: bool) {
|
||||||
let result =
|
dispatch_platform_audio_event(PlatformAudioEvent::InterruptionEnded { should_resume });
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the focused/on-screen push-to-talk hold state.
|
/// 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).
|
/// Set the preferred audio output route (Android/iOS).
|
||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn set_audio_output_route(route: BridgeAudioRoute) {
|
pub fn set_audio_output_route(route: BridgeAudioRoute) {
|
||||||
runtime().block_on(async {
|
dispatch_platform_audio_event(PlatformAudioEvent::AudioOutputRoute(route.into()));
|
||||||
let _ = session().ios_handle_route_change(route.into()).await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
/// Called from Flutter when the app enters background/foreground.
|
/// Called from Flutter when the app enters background/foreground.
|
||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn record_lifecycle_event(state: String) {
|
pub fn record_lifecycle_event(state: String) {
|
||||||
runtime().block_on(async {
|
dispatch_platform_audio_event(PlatformAudioEvent::Lifecycle { state });
|
||||||
session().record_lifecycle_event(&state).await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user