From b65cedfa95c037280dffdc363c3358895c26ffb7 Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Wed, 20 May 2026 14:29:55 +0900 Subject: [PATCH] fix(android): recover from input stream failures --- .../chanora_audio/src/android_voice_unit.rs | 46 +++++++++++++++---- crates/chanora_bridge/src/api.rs | 12 ++++- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/crates/chanora_audio/src/android_voice_unit.rs b/crates/chanora_audio/src/android_voice_unit.rs index e679947..db97a4b 100644 --- a/crates/chanora_audio/src/android_voice_unit.rs +++ b/crates/chanora_audio/src/android_voice_unit.rs @@ -421,7 +421,7 @@ impl AndroidVoiceUnit { let input_builder = input_builder.set_callback(input_cb); let mut input_stream = match input_builder.open_stream() { - Ok(s) => s, + Ok(s) => Some(s), Err(e) => { // Input-preset fallback ladder (SDD-112 item 6) X // Sharing-mode ladder (SDD-112 item 7), explored @@ -432,14 +432,36 @@ impl AndroidVoiceUnit { error = ?e, "android: primary input stream open failed; entering fallback ladder" ); - Self::open_input_fallback(cfg, &event_tx, capture_state.clone())? + match Self::open_input_fallback(cfg, &event_tx, capture_state.clone()) { + Ok(s) => Some(s), + Err(fallback_err) => { + warn!( + target: "chanora_audio", + error = %fallback_err, + "android: input unavailable after all fallbacks; continuing listen-only with output stream" + ); + None + } + } } }; - let input_perf = perf_from_oboe(input_stream.get_performance_mode()); - let input_share = share_from_oboe(input_stream.get_sharing_mode()); - let input_sample_rate = input_stream.get_sample_rate(); - let input_frames_per_burst = input_stream.get_frames_per_burst(); + let input_perf = input_stream + .as_ref() + .map(|s| perf_from_oboe(s.get_performance_mode())) + .unwrap_or(AchievedPerformanceMode::None); + let input_share = input_stream + .as_ref() + .map(|s| share_from_oboe(s.get_sharing_mode())) + .unwrap_or(AchievedSharingMode::Shared); + let input_sample_rate = input_stream + .as_ref() + .map(|s| s.get_sample_rate()) + .unwrap_or(0); + let input_frames_per_burst = input_stream + .as_mut() + .map(|s| s.get_frames_per_burst()) + .unwrap_or(0); let session_id = None; warn!( target: "chanora_audio", @@ -570,7 +592,7 @@ impl AndroidVoiceUnit { publish_android_audio_diagnostics(diagnostics); Ok(Self { - input: Some(input_stream), + input: input_stream, output: Some(output_stream), input_perf, input_share, @@ -697,8 +719,14 @@ impl AndroidVoiceUnit { impl MobileVoiceAudioBackend for AndroidVoiceUnit { fn start(&mut self) -> Result<(), BackendError> { if let Some(s) = self.input.as_mut() { - s.start() - .map_err(|e| BackendError::LifecycleFailed(format!("input start: {e:?}")))?; + if let Err(e) = s.start() { + warn!( + target: "chanora_audio", + error = ?e, + "android: input start failed; continuing listen-only with output stream" + ); + self.input = None; + } } if let Some(s) = self.output.as_mut() { s.start() diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 359554a..fe12ea3 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -459,10 +459,18 @@ pub async fn connect( identity: None, ready_timeout: Duration::from_secs(15), }; - let snap = runtime() + let snap = match runtime() .spawn(async move { session().connect(cfg).await }) .await - .map_err(|e| task_join_error("connect", e))??; + .map_err(|e| task_join_error("connect", e))? + { + Ok(snap) => snap, + Err(chanora_core::CoreError::AlreadyConnected) => runtime() + .spawn(async { session().snapshot().await }) + .await + .map_err(|e| task_join_error("connect.snapshot", e))??, + Err(e) => return Err(e.into()), + }; Ok(snap.into()) }