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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1306308591;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1322894465;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -432,6 +432,100 @@ fn wire__crate__api__get_transmit_mode_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__handle_interruption_began_impl(
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "handle_interruption_began",
|
||||
port: None,
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
deserializer.end();
|
||||
transform_result_sse::<_, ()>((move || {
|
||||
let output_ok = Result::<_, ()>::Ok({
|
||||
crate::api::handle_interruption_began();
|
||||
})?;
|
||||
Ok(output_ok)
|
||||
})())
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__handle_interruption_ended_impl(
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "handle_interruption_ended",
|
||||
port: None,
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_should_resume = <bool>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
transform_result_sse::<_, ()>((move || {
|
||||
let output_ok = Result::<_, ()>::Ok({
|
||||
crate::api::handle_interruption_ended(api_should_resume);
|
||||
})?;
|
||||
Ok(output_ok)
|
||||
})())
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__handle_route_change_impl(
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "handle_route_change",
|
||||
port: None,
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
deserializer.end();
|
||||
transform_result_sse::<_, ()>((move || {
|
||||
let output_ok = Result::<_, ()>::Ok({
|
||||
crate::api::handle_route_change();
|
||||
})?;
|
||||
Ok(output_ok)
|
||||
})())
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__init_storage_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -1314,6 +1408,14 @@ impl SseDecode for crate::api::BridgeEvent {
|
||||
release_tail_ms: var_releaseTailMs,
|
||||
};
|
||||
}
|
||||
9 => {
|
||||
let mut var_began = <bool>::sse_decode(deserializer);
|
||||
let mut var_shouldResume = <bool>::sse_decode(deserializer);
|
||||
return crate::api::BridgeEvent::InterruptionState {
|
||||
began: var_began,
|
||||
should_resume: var_shouldResume,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
@@ -1515,23 +1617,23 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
9 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
29 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||
15 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
29 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
31 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
32 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||
33 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1545,8 +1647,11 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
8 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len),
|
||||
15 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1719,6 +1824,15 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeEvent {
|
||||
release_tail_ms.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart(),
|
||||
crate::api::BridgeEvent::InterruptionState {
|
||||
began,
|
||||
should_resume,
|
||||
} => [
|
||||
9.into_dart(),
|
||||
began.into_into_dart().into_dart(),
|
||||
should_resume.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart(),
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
@@ -1984,6 +2098,14 @@ impl SseEncode for crate::api::BridgeEvent {
|
||||
<bool>::sse_encode(mute, serializer);
|
||||
<u32>::sse_encode(release_tail_ms, serializer);
|
||||
}
|
||||
crate::api::BridgeEvent::InterruptionState {
|
||||
began,
|
||||
should_resume,
|
||||
} => {
|
||||
<i32>::sse_encode(9, serializer);
|
||||
<bool>::sse_encode(began, serializer);
|
||||
<bool>::sse_encode(should_resume, serializer);
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
|
||||
@@ -102,9 +102,7 @@ impl From<chanora_core::CoreError> for BridgeError {
|
||||
) => BridgeError::ServerRejected { code, message },
|
||||
chanora_core::CoreError::Protocol(p) => BridgeError::Connection(format!("{p}")),
|
||||
chanora_core::CoreError::Audio(a) => BridgeError::Connection(format!("audio: {a}")),
|
||||
chanora_core::CoreError::Storage(s) => {
|
||||
BridgeError::Connection(format!("storage: {s}"))
|
||||
}
|
||||
chanora_core::CoreError::Storage(s) => BridgeError::Connection(format!("storage: {s}")),
|
||||
other => BridgeError::Unmapped(format!("{other}")),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user