feat(ios,p0): iOS P0 platform, audio fixes, channel UX
This commit is contained in:
@@ -68,7 +68,7 @@ pub fn bridge_init() {
|
||||
// export depends on it (DEC-016: user-initiated only, never
|
||||
// auto-upload). It runs alongside whatever platform sink
|
||||
// exists below; both consume the same `tracing` events.
|
||||
let redact_layer = chanora_core::RedactingLogLayer::new(log_sink().clone());
|
||||
let redact_layer = chanora_core::RedactingLogLayer::new(log_sink().clone()).with_sanitizer();
|
||||
|
||||
// On Android, also fan tracing output out to logcat so a user can
|
||||
// see protocol/audio diagnostics via `adb logcat -s chanora`.
|
||||
@@ -146,8 +146,7 @@ pub fn log_file_path_str() -> String {
|
||||
fn log_file_path() -> Option<std::path::PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let base = std::env::var_os("LOCALAPPDATA")
|
||||
.or_else(|| std::env::var_os("APPDATA"))?;
|
||||
let base = std::env::var_os("LOCALAPPDATA").or_else(|| std::env::var_os("APPDATA"))?;
|
||||
Some(
|
||||
std::path::PathBuf::from(base)
|
||||
.join("app.chanora")
|
||||
@@ -167,12 +166,18 @@ fn log_file_path() -> Option<std::path::PathBuf> {
|
||||
.join("chanora.log"),
|
||||
)
|
||||
}
|
||||
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))]
|
||||
#[cfg(all(
|
||||
unix,
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
{
|
||||
let base = std::env::var_os("XDG_STATE_HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.or_else(|| {
|
||||
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local").join("state"))
|
||||
std::env::var_os("HOME")
|
||||
.map(|h| std::path::PathBuf::from(h).join(".local").join("state"))
|
||||
})?;
|
||||
Some(
|
||||
base.join("app.chanora")
|
||||
@@ -343,7 +348,11 @@ pub async fn connect(
|
||||
let cfg = chanora_core::ConnectConfig {
|
||||
address: host,
|
||||
nickname,
|
||||
password: if password.is_empty() { None } else { Some(password) },
|
||||
password: if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
},
|
||||
identity: None,
|
||||
ready_timeout: Duration::from_secs(15),
|
||||
};
|
||||
@@ -386,6 +395,34 @@ pub async fn is_connected() -> bool {
|
||||
// flows through `voice_join` / `voice_leave`, which transparently
|
||||
// drive `AudioEngine::ensure_running` / `shutdown_if_idle`.
|
||||
|
||||
/// Handle iOS AVAudioSession route changes (SDD-100).
|
||||
#[frb(sync)]
|
||||
pub fn handle_route_change() {
|
||||
let result = runtime().block_on(async { session().ios_handle_route_change().await });
|
||||
if let Err(e) = result {
|
||||
warn!(target: "chanora_bridge", error = %e, "iOS route-change handling failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the push-to-talk state.
|
||||
///
|
||||
/// Superseded in v1 by [`set_transmit_mode`] + the binding capture
|
||||
@@ -444,7 +481,11 @@ fn transmit_mode_from_u8(v: u8) -> BridgeTransmitMode {
|
||||
/// brings up the audio engine if needed, and emits
|
||||
/// `BridgeEvent::VoiceState`. `password` may be empty.
|
||||
pub async fn voice_join(channel_id: u64, password: String) -> Result<(), BridgeError> {
|
||||
let pw = if password.is_empty() { None } else { Some(password) };
|
||||
let pw = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
};
|
||||
runtime()
|
||||
.spawn(async move { session().voice_join(channel_id, pw).await })
|
||||
.await
|
||||
@@ -578,7 +619,11 @@ pub async fn get_ptt_binding() -> (String, String) {
|
||||
/// for password-protected channels — pass an empty string when not
|
||||
/// required.
|
||||
pub async fn move_to_channel(channel_id: u64, password: String) -> Result<(), BridgeError> {
|
||||
let pw = if password.is_empty() { None } else { Some(password) };
|
||||
let pw = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
};
|
||||
runtime()
|
||||
.spawn(async move { session().move_to_channel(channel_id, pw).await })
|
||||
.await
|
||||
@@ -640,9 +685,15 @@ pub struct BridgeAudioStats {
|
||||
#[frb(sync)]
|
||||
pub fn export_diagnostics() -> String {
|
||||
let metadata = vec![
|
||||
("crate_version".to_string(), env!("CARGO_PKG_VERSION").to_string()),
|
||||
(
|
||||
"crate_version".to_string(),
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
),
|
||||
("target_os".to_string(), std::env::consts::OS.to_string()),
|
||||
("target_arch".to_string(), std::env::consts::ARCH.to_string()),
|
||||
(
|
||||
"target_arch".to_string(),
|
||||
std::env::consts::ARCH.to_string(),
|
||||
),
|
||||
];
|
||||
match chanora_core::DiagnosticExport::from_sink(log_sink(), metadata) {
|
||||
Ok(exp) => exp.to_text(),
|
||||
@@ -858,6 +909,13 @@ pub enum BridgeEvent {
|
||||
/// Current release-tail in milliseconds (0..=500).
|
||||
release_tail_ms: u32,
|
||||
},
|
||||
/// iOS audio interruption state (SDD-101).
|
||||
InterruptionState {
|
||||
/// True when interruption began, false when it ended.
|
||||
began: bool,
|
||||
/// Resume recommendation from the platform. False on begin.
|
||||
should_resume: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<chanora_core::SessionEvent> for BridgeEvent {
|
||||
@@ -902,6 +960,13 @@ impl From<chanora_core::SessionEvent> for BridgeEvent {
|
||||
mute,
|
||||
release_tail_ms,
|
||||
},
|
||||
chanora_core::SessionEvent::InterruptionState {
|
||||
began,
|
||||
should_resume,
|
||||
} => BridgeEvent::InterruptionState {
|
||||
began,
|
||||
should_resume,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user